Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
P
packer
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kristopher Ruzic
packer
Commits
8ba89325
Commit
8ba89325
authored
Jun 14, 2013
by
Jack Pearkes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
builder/digitalocean: No need for destroy steps, builder works!
parent
a774e2b4
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
118 additions
and
117 deletions
+118
-117
builder/digitalocean/api.go
builder/digitalocean/api.go
+28
-12
builder/digitalocean/builder.go
builder/digitalocean/builder.go
+2
-4
builder/digitalocean/step_create_droplet.go
builder/digitalocean/step_create_droplet.go
+8
-1
builder/digitalocean/step_create_ssh_key.go
builder/digitalocean/step_create_ssh_key.go
+72
-33
builder/digitalocean/step_destroy_droplet.go
builder/digitalocean/step_destroy_droplet.go
+0
-29
builder/digitalocean/step_destroy_ssh_key.go
builder/digitalocean/step_destroy_ssh_key.go
+0
-29
builder/digitalocean/step_power_off.go
builder/digitalocean/step_power_off.go
+6
-0
builder/digitalocean/wait.go
builder/digitalocean/wait.go
+2
-9
No files found.
builder/digitalocean/api.go
View file @
8ba89325
...
...
@@ -9,7 +9,9 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
)
const
DIGITALOCEAN_API_URL
=
"https://api.digitalocean.com"
...
...
@@ -39,7 +41,10 @@ func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
// Creates an SSH Key and returns it's id
func
(
d
DigitalOceanClient
)
CreateKey
(
name
string
,
pub
string
)
(
uint
,
error
)
{
params
:=
fmt
.
Sprintf
(
"?name=%s&ssh_pub_key=%s"
,
name
,
pub
)
// Escape the public key
pub
=
url
.
QueryEscape
(
pub
)
params
:=
fmt
.
Sprintf
(
"name=%v&ssh_pub_key=%v"
,
name
,
pub
)
body
,
err
:=
NewRequest
(
d
,
"ssh_keys/new"
,
params
)
if
err
!=
nil
{
...
...
@@ -54,7 +59,7 @@ func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
// Destroys an SSH key
func
(
d
DigitalOceanClient
)
DestroyKey
(
id
uint
)
error
{
path
:=
fmt
.
Sprintf
(
"ssh_keys/%
s
/destroy"
,
id
)
path
:=
fmt
.
Sprintf
(
"ssh_keys/%
v
/destroy"
,
id
)
_
,
err
:=
NewRequest
(
d
,
path
,
""
)
return
err
}
...
...
@@ -62,8 +67,8 @@ func (d DigitalOceanClient) DestroyKey(id uint) error {
// Creates a droplet and returns it's id
func
(
d
DigitalOceanClient
)
CreateDroplet
(
name
string
,
size
uint
,
image
uint
,
region
uint
,
keyId
uint
)
(
uint
,
error
)
{
params
:=
fmt
.
Sprintf
(
"name=%
s&size_id=%s&image_id=%s&size_id=%s&image_id=%s®ion_id=%s&ssh_key_ids=%s
"
,
name
,
size
,
image
,
size
,
region
,
keyId
)
"name=%
v&image_id=%v&size_id=%v®ion_id=%v&ssh_key_ids=%v
"
,
name
,
image
,
size
,
region
,
keyId
)
body
,
err
:=
NewRequest
(
d
,
"droplets/new"
,
params
)
if
err
!=
nil
{
...
...
@@ -78,14 +83,14 @@ func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, re
// Destroys a droplet
func
(
d
DigitalOceanClient
)
DestroyDroplet
(
id
uint
)
error
{
path
:=
fmt
.
Sprintf
(
"droplets/%
s
/destroy"
,
id
)
path
:=
fmt
.
Sprintf
(
"droplets/%
v
/destroy"
,
id
)
_
,
err
:=
NewRequest
(
d
,
path
,
""
)
return
err
}
// Powers off a droplet
func
(
d
DigitalOceanClient
)
PowerOffDroplet
(
id
uint
)
error
{
path
:=
fmt
.
Sprintf
(
"droplets/%
s
/power_off"
,
id
)
path
:=
fmt
.
Sprintf
(
"droplets/%
v
/power_off"
,
id
)
_
,
err
:=
NewRequest
(
d
,
path
,
""
)
...
...
@@ -94,8 +99,8 @@ func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
// Creates a snaphot of a droplet by it's ID
func
(
d
DigitalOceanClient
)
CreateSnapshot
(
id
uint
,
name
string
)
error
{
path
:=
fmt
.
Sprintf
(
"droplets/%
s
/snapshot"
,
id
)
params
:=
fmt
.
Sprintf
(
"name=%
s
"
,
name
)
path
:=
fmt
.
Sprintf
(
"droplets/%
v
/snapshot"
,
id
)
params
:=
fmt
.
Sprintf
(
"name=%
v
"
,
name
)
_
,
err
:=
NewRequest
(
d
,
path
,
params
)
...
...
@@ -104,17 +109,22 @@ func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
// Returns DO's string representation of status "off" "new" "active" etc.
func
(
d
DigitalOceanClient
)
DropletStatus
(
id
uint
)
(
string
,
string
,
error
)
{
path
:=
fmt
.
Sprintf
(
"droplets/%
s
"
,
id
)
path
:=
fmt
.
Sprintf
(
"droplets/%
v
"
,
id
)
body
,
err
:=
NewRequest
(
d
,
path
,
""
)
if
err
!=
nil
{
return
""
,
""
,
err
}
var
ip
string
// Read the droplet's "status"
droplet
:=
body
[
"droplet"
]
.
(
map
[
string
]
interface
{})
status
:=
droplet
[
"status"
]
.
(
string
)
ip
:=
droplet
[
"ip_address"
]
.
(
string
)
if
droplet
[
"ip_address"
]
!=
nil
{
ip
=
droplet
[
"ip_address"
]
.
(
string
)
}
return
ip
,
status
,
err
}
...
...
@@ -123,11 +133,13 @@ func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
// the response.
func
NewRequest
(
d
DigitalOceanClient
,
path
string
,
params
string
)
(
map
[
string
]
interface
{},
error
)
{
client
:=
d
.
client
url
:=
fmt
.
Sprintf
(
"%
s/%s?%s&client_id=%s&api_key=%s
"
,
url
:=
fmt
.
Sprintf
(
"%
v/%v?%v&client_id=%v&api_key=%v
"
,
DIGITALOCEAN_API_URL
,
path
,
params
,
d
.
ClientID
,
d
.
APIKey
)
var
decodedResponse
map
[
string
]
interface
{}
log
.
Printf
(
"sending new request to digitalocean: %v"
,
url
)
resp
,
err
:=
client
.
Get
(
url
)
if
err
!=
nil
{
return
decodedResponse
,
err
...
...
@@ -140,13 +152,16 @@ func NewRequest(d DigitalOceanClient, path string, params string) (map[string]in
return
decodedResponse
,
err
}
err
=
json
.
Unmarshal
(
body
,
&
decodedResponse
)
// Catch all non-200 status and return an error
if
resp
.
StatusCode
!=
200
{
err
=
errors
.
New
(
fmt
.
Sprintf
(
"recieved non-200 status from digitalocean: %d"
,
resp
.
StatusCode
))
log
.
Printf
(
"response from digital ocean: %v"
,
decodedResponse
)
return
decodedResponse
,
err
}
err
=
json
.
Unmarshal
(
body
,
&
decodedResponse
)
log
.
Printf
(
"response from digital ocean: %v"
,
decodedResponse
)
if
err
!=
nil
{
return
decodedResponse
,
err
...
...
@@ -156,6 +171,7 @@ func NewRequest(d DigitalOceanClient, path string, params string) (map[string]in
status
:=
decodedResponse
[
"status"
]
if
status
!=
"OK"
{
err
=
errors
.
New
(
fmt
.
Sprintf
(
"recieved non-OK status from digitalocean: %d"
,
status
))
log
.
Printf
(
"response from digital ocean: %v"
,
decodedResponse
)
return
decodedResponse
,
err
}
...
...
builder/digitalocean/builder.go
View file @
8ba89325
...
...
@@ -60,8 +60,8 @@ func (b *Builder) Prepare(raw interface{}) error {
}
if
b
.
config
.
ImageID
==
0
{
// Default to base image "Ubuntu 12.04 x64 Server"
b
.
config
.
ImageID
=
2
676
// Default to base image "Ubuntu 12.04 x64 Server
(id: 284203)
"
b
.
config
.
ImageID
=
2
84203
}
if
b
.
config
.
SSHUsername
==
""
{
...
...
@@ -131,8 +131,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new
(
stepProvision
),
new
(
stepPowerOff
),
new
(
stepSnapshot
),
new
(
stepDestroyDroplet
),
new
(
stepDestroySSHKey
),
}
// Run the steps
...
...
builder/digitalocean/step_create_droplet.go
View file @
8ba89325
...
...
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"time"
)
type
stepCreateDroplet
struct
{
...
...
@@ -51,10 +52,16 @@ func (s *stepCreateDroplet) Cleanup(state map[string]interface{}) {
// Destroy the droplet we just created
ui
.
Say
(
"Destroying droplet..."
)
// Sleep arbitrarily before sending destroy request
// Otherwise we get "pending event" errors, even though there isn't
// one.
time
.
Sleep
(
5
*
time
.
Second
)
err
:=
client
.
DestroyDroplet
(
s
.
dropletId
)
if
err
!=
nil
{
ui
.
Error
(
fmt
.
Sprintf
(
"Error destroying droplet. Please destroy it manually: %
s
"
,
s
.
dropletId
))
"Error destroying droplet. Please destroy it manually: %
v
"
,
s
.
dropletId
))
}
}
builder/digitalocean/step_create_ssh_key.go
View file @
8ba89325
...
...
@@ -2,11 +2,7 @@ package digitalocean
import
(
"cgl.tideland.biz/identifier"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
...
...
@@ -22,37 +18,80 @@ func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepActio
ui
:=
state
[
"ui"
]
.
(
packer
.
Ui
)
ui
.
Say
(
"Creating temporary ssh key for droplet..."
)
priv
,
err
:=
rsa
.
GenerateKey
(
rand
.
Reader
,
2014
)
if
err
!=
nil
{
ui
.
Error
(
err
.
Error
())
return
multistep
.
ActionHalt
}
// priv, err := rsa.GenerateKey(rand.Reader, 2014)
// if err != nil {
// ui.Error(err.Error())
// return multistep.ActionHalt
// }
// priv_der := x509.MarshalPKCS1PrivateKey(priv)
// priv_blk := pem.Block{
// Type: "RSA PRIVATE KEY",
// Headers: nil,
// Bytes: priv_der,
// }
// Set the pem formatted private key on the state for later
priv_der
:=
x509
.
MarshalPKCS1PrivateKey
(
priv
)
priv_blk
:=
pem
.
Block
{
Type
:
"RSA PRIVATE KEY"
,
Headers
:
nil
,
Bytes
:
priv_der
,
}
// state["privateKey"] = string(pem.EncodeToMemory(&priv_blk))
// log.Printf("PRIVATE KEY:\n\n%v\n\n", state["privateKey"])
// Create the public key for uploading to DO
pub
:=
priv
.
PublicKey
pub_der
,
err
:=
x509
.
MarshalPKIXPublicKey
(
&
pub
)
if
err
!=
nil
{
ui
.
Error
(
err
.
Error
())
return
multistep
.
ActionHalt
}
pub_blk
:=
pem
.
Block
{
Type
:
"PUBLIC KEY"
,
Headers
:
nil
,
Bytes
:
pub_der
,
}
pub_pem
:=
string
(
pem
.
EncodeToMemory
(
&
pub_blk
))
name
:=
fmt
.
Sprintf
(
"packer %s"
,
hex
.
EncodeToString
(
identifier
.
NewUUID
()
.
Raw
()))
keyId
,
err
:=
client
.
CreateKey
(
name
,
pub_pem
)
// pub := priv.PublicKey
// pub_bytes, err := x509.MarshalPKIXPublicKey(&pub)
// pub_blk := pem.Block{
// Type: "RSA PUBLIC KEY",
// Headers: nil,
// Bytes: pub_bytes,
// }
// if err != nil {
// ui.Error(err.Error())
// return multistep.ActionHalt
// }
// // Encode the public key to base64
// pub_str := base64.StdEncoding.EncodeToString(pub_bytes)
// pub_str = "ssh-rsa " + pub_str
// log.Printf("PUBLIC KEY:\n\n%v\n\n", string(pem.EncodeToMemory(&pub_blk)))
// log.Printf("PUBLIC KEY BASE64:\n\n%v\n\n", pub_str)
pub_str
:=
`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD29LZNMe0f7nOmdOIXDrF6eAmLZEk1yrnnsPI+xjLsnKxggMjdD3HvkBPXMdhakOj3pEF6DNtXbK43A7Pilezvu7y2awz+dxCavgUNtwaJkiTJw3C2qleNDDgrq7ZYLJ/wKmfhgPO4jZBej/8ONA0VjxemCNBPTTBeZ8FaeOpeUqopdhk78KGeGmUJ8Bvl8ACuYNdtJ5Y0BQCZkJT+g1ntTwHvuq/Vy/E2uCwJ2xV3vCDkLlqXVyksuVIcLJxTPtd5LdasD4WMQwoOPNdNMBLBG6ZBhXC/6kCVbMgzy5poSZ7r6BK0EA6b2EdAanaojYs3i52j6JeCIIrYtu9Ub173 jack@jose.local`
state
[
"privateKey"
]
=
`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA9vS2TTHtH+5zpnTiFw6xengJi2RJNcq557DyPsYy7JysYIDI
3Q9x75AT1zHYWpDo96RBegzbV2yuNwOz4pXs77u8tmsM/ncQmr4FDbcGiZIkycNw
tqpXjQw4K6u2WCyf8Cpn4YDzuI2QXo//DjQNFY8XpgjQT00wXmfBWnjqXlKqKXYZ
O/ChnhplCfAb5fAArmDXbSeWNAUAmZCU/oNZ7U8B77qv1cvxNrgsCdsVd7wg5C5a
l1cpLLlSHCycUz7XeS3WrA+FjEMKDjzXTTASwRumQYVwv+pAlWzIM8uaaEme6+gS
tBAOm9hHQGp2qI2LN4udo+iXgiCK2LbvVG9e9wIDAQABAoIBABuBB6izTciHoyO/
0spknYmZQt7ebXTrPic6wtAQ/OzzShN5ZGWSacsXjc4ixAjaKMgj6BLyyZ8EAKcp
52ft8LSGgS8D3y+cDSJe1WtAnh7GQwihlrURZazU1pCukCFj3vA9mNI5rWs5gQG3
Id3wGCD1jdm1E5Yxb5ikD5nG67tTW5Pn4+tidsavTNsDLsks/pW/0EcPcKAS+TJ8
Zy15MsGGfHVVkxf+ldULIxxidAeplQhWuED6wkbuD3LQi6Kt4yElHS+UCATca8Fe
CvXNcQWrEHiYUvpyrvU3ybw7WEUUWFa/dctSZwmHvkvRD/bwJPf5M8sIIl8zlyuy
3YCIlSkCgYEA/ZqGOnYIK/bA/QVuyFkFkP3aJjOKJtH0RV9V5XVKSBlU1/Lm3DUZ
XVmp7JuWZHVhPxZa8tswj4x15dX+TwTvGdoUuqPC7K/UMOt6Qzk11o0+o2VRYU97
GzYyEDxGEnRqoZsc1922I6nBv8YqsW4WkMRhkFN4JNzLJBVXMTXcDCMCgYEA+Uob
VQfVF+7BfCOCNdSu9dqZoYRCyBm5JNEp5bqF1kiEbGw4FhJYp95Ix5ogD3Ug4aqe
8ylwUK86U2BhfkKmGQ5yf+6VNoTx3EPFaGrODIi82BUraYPyYEN10ZrR8Czy5X9g
1WC+WuboRgvTZs+grwnDVJwqQIOqIB2L0p+SdR0CgYEAokHavc7E/bP72CdAsSjb
+d+hUq3JJ3tPiY8suwnnQ+gJM72y3ZOPrf1vTfZiK9Y6KQ4ZlKaPFFkvGaVn95DV
ljnE54FddugsoDwZVqdk/egS+qIZhmQ/BLMRJvgZcTdQ/iLrOmYdYgX788JLkIg6
Ide0AI6XISavRl/tEIxARPcCgYEAlgh+6K8dFhlRA7iPPnyxjDAzdF0YoDuzDTCB
icy3jh747BQ5sTb7epSyssbU8tiooIjCv1A6U6UScmm4Y3gTZVMnoE1kKnra4Zk8
LzrQpgSJu3cKOKf78OnI+Ay4u1ciHPOLwQBHsIf2VWn6oo7lg1NZ5wtR9qAHfOqr
Y2k8iRUCgYBKQCtY4SNDuFb6+r5YSEFVfelCn6DJzNgTxO2mkUzzM7RcgejHbd+i
oqgnYXsFLJgm+NpN1eFpbs2RgAe8Zd4pKQNwJFJf0EbEP57sW3kujgFFEsPYJPOp
n8wFU32yrKgrVCftmCk1iI+WPfr1r9LKgKhb0sRX1+DsdWqfN6J7Sw==
-----END RSA PRIVATE KEY-----`
// The name of the public key on DO
name
:=
fmt
.
Sprintf
(
"packer-%s"
,
hex
.
EncodeToString
(
identifier
.
NewUUID
()
.
Raw
()))
// Create the key!
keyId
,
err
:=
client
.
CreateKey
(
name
,
pub_str
)
if
err
!=
nil
{
ui
.
Error
(
err
.
Error
())
...
...
@@ -66,7 +105,6 @@ func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepActio
// Remember some state for the future
state
[
"ssh_key_id"
]
=
keyId
state
[
"privateKey"
]
=
string
(
pem
.
EncodeToMemory
(
&
priv_blk
))
return
multistep
.
ActionContinue
}
...
...
@@ -83,7 +121,8 @@ func (s *stepCreateSSHKey) Cleanup(state map[string]interface{}) {
ui
.
Say
(
"Deleting temporary ssh key..."
)
err
:=
client
.
DestroyKey
(
s
.
keyId
)
if
err
!=
nil
{
log
.
Printf
(
"Error cleaning up ssh key: %v"
,
err
.
Error
())
ui
.
Error
(
fmt
.
Sprintf
(
"Error cleaning up ssh key. Please delete the key manually: %
s
"
,
s
.
keyId
))
"Error cleaning up ssh key. Please delete the key manually: %
v
"
,
s
.
keyId
))
}
}
builder/digitalocean/step_destroy_droplet.go
deleted
100644 → 0
View file @
a774e2b4
package
digitalocean
import
(
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type
stepDestroyDroplet
struct
{}
func
(
s
*
stepDestroyDroplet
)
Run
(
state
map
[
string
]
interface
{})
multistep
.
StepAction
{
client
:=
state
[
"client"
]
.
(
*
DigitalOceanClient
)
ui
:=
state
[
"ui"
]
.
(
packer
.
Ui
)
dropletId
:=
state
[
"droplet_id"
]
.
(
uint
)
ui
.
Say
(
"Destroying droplet..."
)
err
:=
client
.
DestroyDroplet
(
dropletId
)
if
err
!=
nil
{
ui
.
Error
(
err
.
Error
())
return
multistep
.
ActionHalt
}
return
multistep
.
ActionContinue
}
func
(
s
*
stepDestroyDroplet
)
Cleanup
(
state
map
[
string
]
interface
{})
{
// no cleanup
}
builder/digitalocean/step_destroy_ssh_key.go
deleted
100644 → 0
View file @
a774e2b4
package
digitalocean
import
(
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type
stepDestroySSHKey
struct
{}
func
(
s
*
stepDestroySSHKey
)
Run
(
state
map
[
string
]
interface
{})
multistep
.
StepAction
{
client
:=
state
[
"client"
]
.
(
*
DigitalOceanClient
)
ui
:=
state
[
"ui"
]
.
(
packer
.
Ui
)
sshKeyId
:=
state
[
"ssh_key_id"
]
.
(
uint
)
ui
.
Say
(
"Destroying temporary ssh key..."
)
err
:=
client
.
DestroyKey
(
sshKeyId
)
if
err
!=
nil
{
ui
.
Error
(
err
.
Error
())
return
multistep
.
ActionHalt
}
return
multistep
.
ActionContinue
}
func
(
s
*
stepDestroySSHKey
)
Cleanup
(
state
map
[
string
]
interface
{})
{
// no cleanup
}
builder/digitalocean/step_power_off.go
View file @
8ba89325
...
...
@@ -3,6 +3,7 @@ package digitalocean
import
(
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"time"
)
type
stepPowerOff
struct
{}
...
...
@@ -12,6 +13,11 @@ func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction {
ui
:=
state
[
"ui"
]
.
(
packer
.
Ui
)
dropletId
:=
state
[
"droplet_id"
]
.
(
uint
)
// Sleep arbitrarily before sending power off request
// Otherwise we get "pending event" errors, even though there isn't
// one.
time
.
Sleep
(
3
*
time
.
Second
)
// Poweroff the droplet so it can be snapshot
err
:=
client
.
PowerOffDroplet
(
dropletId
)
...
...
builder/digitalocean/wait.go
View file @
8ba89325
...
...
@@ -14,10 +14,6 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce
go
func
()
{
attempts
:=
0
for
{
select
{
default
:
}
attempts
+=
1
log
.
Printf
(
"Checking droplet status... (attempt: %d)"
,
attempts
)
...
...
@@ -33,8 +29,8 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce
break
}
// Wait
a second
in between
time
.
Sleep
(
1
*
time
.
Second
)
// Wait
3 seconds
in between
time
.
Sleep
(
3
*
time
.
Second
)
}
active
<-
true
...
...
@@ -53,9 +49,6 @@ ActiveWaitLoop:
case
<-
timeout
:
err
:=
errors
.
New
(
"Timeout while waiting to for droplet to become active"
)
return
err
case
<-
time
.
After
(
1
*
time
.
Second
)
:
err
:=
errors
.
New
(
"Interrupt detected, quitting waiting for droplet"
)
return
err
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment