Skip to content

Commit ad85aba

Browse files
timfparkcarmstrong
authored andcommitted
feat(contrib/azure): add Azure provision scripts and docs
1 parent f030781 commit ad85aba

8 files changed

Lines changed: 495 additions & 0 deletions

File tree

contrib/azure/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Deis on Microsoft Azure
2+
3+
Please refer to the instructions at http://docs.deis.io/en/latest/installing_deis/azure/.

contrib/azure/azure-coreos-cluster

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env python
2+
3+
from azure import *
4+
from azure.servicemanagement import *
5+
import argparse
6+
import urllib2
7+
import time
8+
import base64
9+
import os
10+
import subprocess
11+
12+
parser = argparse.ArgumentParser(description='Create a CoreOS cluster on Microsoft Azure.')
13+
parser.add_argument('--version', action='version', version='azure-coreos-cluster 0.1')
14+
parser.add_argument('cloud_service_name',
15+
help='cloud service name')
16+
parser.add_argument('--ssh-cert',
17+
help='certificate file with public key for ssh, in .cer format')
18+
parser.add_argument('--ssh-thumb',
19+
help='thumbprint of ssh cert')
20+
parser.add_argument('--subscription', required=True,
21+
help='required Azure subscription id')
22+
parser.add_argument('--azure-cert', required=True,
23+
help='required path to Azure cert pem file')
24+
parser.add_argument('--blob-container-url', required=True,
25+
help='required url to blob container where vm disk images will be created, including /, ex: https://patcoreos.blob.core.windows.net/vhds/')
26+
parser.add_argument('--vm-size', default='Small',
27+
help='optional, VM size [Small]')
28+
parser.add_argument('--vm-name-prefix', default='coreos',
29+
help='optional, VM name prefix [coreos]')
30+
parser.add_argument('--availability-set', default='coreos-as',
31+
help='optional, name of availability set for cluster [coreos-as]')
32+
parser.add_argument('--location', default='West US',
33+
help='optional, [West US]')
34+
parser.add_argument('--ssh', default=22001, type=int,
35+
help='optional, starts with 22001 and +1 for each machine in cluster')
36+
parser.add_argument('--coreos-image', default='2b171e93f07c4903bcad35bda10acf22__CoreOS-Beta-494.1.0',
37+
help='optional, [2b171e93f07c4903bcad35bda10acf22__CoreOS-Beta-494.1.0]')
38+
parser.add_argument('--num-nodes', default=3, type=int,
39+
help='optional, number of nodes to create (or add), defaults to 3')
40+
parser.add_argument('--virtual-network-name',
41+
help='optional, name of an existing virtual network to which we will add the VMs')
42+
parser.add_argument('--subnet-names',
43+
help='optional, subnet name to which the VMs will belong')
44+
parser.add_argument('--custom-data',
45+
help='optional, path to your own cloud-init file')
46+
parser.add_argument('--discovery-service-url',
47+
help='optional, url for an existing cluster discovery service. Else we will generate one.')
48+
parser.add_argument('--pip', action='store_true',
49+
help='optional, assigns public instance ip addresses to each VM')
50+
parser.add_argument('--deis', action='store_true',
51+
help='optional, automatically opens http and controller endpoints')
52+
parser.add_argument('--data-disk', action='store_true',
53+
help='optional, attaches a data disk to each VM')
54+
55+
cloud_init_template = """#cloud-config
56+
57+
coreos:
58+
etcd:
59+
# generate a new token for each unique cluster from https://discovery.etcd.io/new
60+
discovery: {0}
61+
# deployments across multiple cloud services will need to use $public_ipv4
62+
addr: $private_ipv4:4001
63+
peer-addr: $private_ipv4:7001
64+
units:
65+
- name: etcd.service
66+
command: start
67+
- name: fleet.service
68+
command: start
69+
"""
70+
71+
args = parser.parse_args()
72+
73+
# Create SSH cert if it's not given
74+
if not args.ssh_cert and not args.ssh_thumb:
75+
print 'SSH arguments not given, generating certificate'
76+
with open(os.devnull, 'w') as shutup:
77+
subprocess.call('openssl req -x509 -nodes -days 365 -newkey rsa:2048 -config cert.conf -keyout ssh-cert.key -out ssh-cert.pem', shell=True, stdout=shutup, stderr=shutup)
78+
subprocess.call('chmod 600 ssh-cert.key', shell=True, stdout=shutup, stderr=shutup)
79+
subprocess.call('openssl x509 -outform der -in ssh-cert.pem -out ssh-cert.cer', shell=True, stdout=shutup, stderr=shutup)
80+
thumbprint = subprocess.check_output('openssl x509 -in ssh-cert.pem -sha1 -noout -fingerprint | sed s/://g', shell=True)
81+
args.ssh_thumb = thumbprint.split('=')[1].replace('\n', '')
82+
args.ssh_cert = './ssh-cert.cer'
83+
print 'Generated SSH certificate with thumbprint ' + args.ssh_thumb
84+
85+
# Setup custom data
86+
if args.custom_data:
87+
with open(args.custom_data, 'r') as f:
88+
cloud_init = f.read()
89+
f.closed
90+
else:
91+
if args.discovery_service_url:
92+
cloud_init = cloud_init_template.format(args.discovery_service_url)
93+
else:
94+
response = urllib2.urlopen('https://discovery.etcd.io/new')
95+
discovery_url = response.read()
96+
cloud_init = cloud_init_template.format(discovery_url)
97+
98+
SERVICE_CERT_FORMAT = 'pfx'
99+
100+
with open(args.ssh_cert) as f:
101+
service_cert_file_data = base64.b64encode(f.read())
102+
f.closed
103+
104+
def wait_for_async(request_id, operation_name, timeout):
105+
count = 0
106+
result = sms.get_operation_status(request_id)
107+
while result.status == 'InProgress':
108+
count = count + 1
109+
if count > timeout:
110+
print('Timed out waiting for async operation to complete.')
111+
return
112+
time.sleep(5)
113+
print('.')
114+
result = sms.get_operation_status(request_id)
115+
print(vars(result))
116+
if result.error:
117+
print(result.error.code)
118+
print(vars(result.error))
119+
print(result.status)
120+
print(operation_name + ' took:' + str(count*5) + 's')
121+
122+
def linux_config(hostname, args):
123+
pk = PublicKey(args.ssh_thumb,
124+
u'/home/core/.ssh/authorized_keys')
125+
system = LinuxConfigurationSet(hostname, 'core', None, True,
126+
custom_data=cloud_init)
127+
system.ssh.public_keys.public_keys.append(pk)
128+
system.disable_ssh_password_authentication = True
129+
return system
130+
131+
def endpoint_config(name, port):
132+
endpoint = ConfigurationSetInputEndpoint(name, 'tcp', port, port, name)
133+
load_balancer_probe = LoadBalancerProbe()
134+
load_balancer_probe.path = 'health-check'
135+
load_balancer_probe.port = port
136+
load_balancer_probe.protocol = 'http'
137+
endpoint.load_balancer_probe = load_balancer_probe
138+
return endpoint
139+
140+
def network_config(subnet_name=None, port='59913', public_ip_name=None):
141+
network = ConfigurationSet()
142+
network.configuration_set_type = 'NetworkConfiguration'
143+
network.input_endpoints.input_endpoints.append(
144+
ConfigurationSetInputEndpoint('ssh', 'tcp', port, '22'))
145+
if subnet_name:
146+
network.subnet_names.append(subnet_name)
147+
if public_ip_name:
148+
network.public_ips.public_ips.append(PublicIP(name=public_ip_name))
149+
if args.deis:
150+
network.input_endpoints.input_endpoints.append(endpoint_config('http', '80'))
151+
network.input_endpoints.input_endpoints.append(endpoint_config('deis', '2222'))
152+
return network
153+
154+
def data_hd(target_container_url, target_blob_name, target_lun, target_disk_size_in_gb):
155+
media_link = target_container_url + target_blob_name
156+
data_hd = DataVirtualHardDisk()
157+
data_hd.disk_label = target_blob_name
158+
data_hd.logical_disk_size_in_gb = target_disk_size_in_gb
159+
data_hd.lun = target_lun
160+
data_hd.media_link = media_link
161+
return data_hd
162+
163+
sms = ServiceManagementService(args.subscription, args.azure_cert)
164+
165+
#Create the cloud service
166+
sms.create_hosted_service(
167+
args.cloud_service_name, label=args.cloud_service_name, location=args.location)
168+
print('created service ' + args.cloud_service_name)
169+
time.sleep(2)
170+
171+
#upload ssh cert to cloud-service
172+
result = sms.add_service_certificate(args.cloud_service_name,
173+
service_cert_file_data, SERVICE_CERT_FORMAT, '')
174+
wait_for_async(result.request_id, 'upload cert', 15)
175+
176+
def get_vm_name(args, i):
177+
return args.cloud_service_name + '-' + args.vm_name_prefix + '-' + str(i)
178+
179+
vms =[]
180+
181+
#Create the VMs
182+
for i in range(args.num_nodes):
183+
ssh_port = args.ssh +i
184+
vm_name = get_vm_name(args, i)
185+
if args.pip:
186+
pip_name = vm_name
187+
else:
188+
pip_name = None
189+
media_link = args.blob_container_url + vm_name
190+
os_hd = OSVirtualHardDisk(media_link=media_link,
191+
source_image_name=args.coreos_image)
192+
system = linux_config(vm_name, args)
193+
network = network_config(subnet_name=args.subnet_names, port=ssh_port, public_ip_name=pip_name)
194+
#specifiy the data disk, important to start at lun = 0
195+
if args.data_disk:
196+
data_disk = data_hd(args.blob_container_url, vm_name + '-data.vhd', 0, 25)
197+
data_disks = DataVirtualHardDisks()
198+
data_disks.data_virtual_hard_disks.append(data_disk)
199+
else:
200+
data_disks = None
201+
202+
if i == 0:
203+
result = sms.create_virtual_machine_deployment(
204+
args.cloud_service_name, deployment_name=args.cloud_service_name,
205+
deployment_slot='production', label=vm_name,
206+
role_name=vm_name, system_config=system, os_virtual_hard_disk=os_hd,
207+
role_size=args.vm_size, network_config=network, data_virtual_hard_disks=data_disks)
208+
else:
209+
result = sms.add_role(
210+
args.cloud_service_name, deployment_name=args.cloud_service_name,
211+
role_name=vm_name,
212+
system_config=system, os_virtual_hard_disk=os_hd,
213+
role_size=args.vm_size, network_config=network, data_virtual_hard_disks=data_disks)
214+
wait_for_async(result.request_id, 'create VM' + vm_name, 30)
215+
vms.append({'name':vm_name,
216+
'host':args.cloud_service_name + '.cloudapp.net',
217+
'port':ssh_port,
218+
'user':'core',
219+
'identity':args.ssh_cert.replace('.cer','.key')})
220+
221+
#get the ip addresses
222+
def get_ips(service_name, deployment_name):
223+
result = sms.get_deployment_by_name(service_name, deployment_name)
224+
for instance in result.role_instance_list:
225+
ips.append(instance.public_ips[0].address)
226+
return ips
227+
228+
#print dns config
229+
if args.pip:
230+
ips = []
231+
ips = get_ips(args.cloud_service_name, args.cloud_service_name)
232+
print 'dns file ----'
233+
for ip in ips:
234+
print '@ 10800 IN A ' + ip
235+
print '* 10800 IN CNAME @'
236+
print 'end dns file ----'
237+
238+
#print ~/.ssh/config
239+
print '~/.ssh/config ----'
240+
for vm in vms:
241+
print 'Host ' + vm['name']
242+
print ' HostName ' + vm['host']
243+
print ' Port ' + str(vm['port'])
244+
print ' User ' + vm['user']
245+
print ' IdentityFile ' + vm['identity']
246+
print 'end ~/.ssh/config ----'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#cloud-config
2+
coreos:
3+
units:
4+
- name: format-ephemeral.service
5+
command: start
6+
content: |
7+
[Unit]
8+
Description=Formats the ephemeral drive
9+
ConditionPathExists=!/etc/azure-formatted
10+
[Service]
11+
Type=oneshot
12+
RemainAfterExit=yes
13+
ExecStart=/usr/sbin/wipefs -f /dev/sdc
14+
ExecStart=/usr/sbin/mkfs.btrfs -f /dev/sdc
15+
ExecStart=/bin/touch /etc/azure-formatted
16+
- name: var-lib-docker.mount
17+
command: start
18+
content: |
19+
[Unit]
20+
Description=Mount ephemeral to /var/lib/docker
21+
Requires=format-ephemeral.service
22+
After=format-ephemeral.service
23+
Before=docker.service
24+
[Mount]
25+
What=/dev/sdc
26+
Where=/var/lib/docker
27+
Type=btrfs

contrib/azure/cert.conf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[ req ]
2+
prompt = no
3+
default_bits = 2048
4+
encrypt_key = no
5+
distinguished_name = req_distinguished_name
6+
7+
string_mask = utf8only
8+
9+
[ req_distinguished_name ]
10+
O=My Company
11+
L=San Francisco
12+
ST=CA
13+
C=US
14+
CN=www.test.com
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env python
2+
"""
3+
Create CoreOS user-data by merging contrib/coreos/user-data and azure-user-data
4+
5+
Usage: create-azure-user-data.py <discovery_url>
6+
7+
Arguments:
8+
<discovery_url> This is the CoreOS etcd discovery URL. You should generate
9+
a new URL at https://discovery.etcd.io/new and pass it as the argument.
10+
"""
11+
12+
import sys
13+
import os
14+
import re
15+
import yaml
16+
import collections
17+
18+
19+
def combine_dicts(orig_dict, new_dict):
20+
for key, val in new_dict.iteritems():
21+
if isinstance(val, collections.Mapping):
22+
tmp = combine_dicts(orig_dict.get(key, {}), val)
23+
orig_dict[key] = tmp
24+
elif isinstance(val, list):
25+
orig_dict[key] = (val + orig_dict[key])
26+
else:
27+
orig_dict[key] = new_dict[key]
28+
return orig_dict
29+
30+
31+
def get_file(name, mode="r", abspath=False):
32+
current_dir = os.path.dirname(__file__)
33+
34+
if abspath:
35+
return file(os.path.abspath(os.path.join(current_dir, name)), mode)
36+
else:
37+
return file(os.path.join(current_dir, name), mode)
38+
39+
40+
def main():
41+
try:
42+
url = sys.argv[1]
43+
except (NameError, IndexError):
44+
print __doc__
45+
return 1
46+
47+
url_pattern = 'http[|s]\:[\/]{2}discovery\.etcd\.io\/[0-9a-f]{32}'
48+
49+
m = re.match(url_pattern, url)
50+
51+
if not m:
52+
print "Discovery URL invalid."
53+
return 1
54+
55+
azure_user_data = get_file("azure-user-data", "w", True)
56+
azure_template = get_file("azure-user-data-template")
57+
coreos_template = get_file("../coreos/user-data.example")
58+
59+
configuration_coreos_template = yaml.safe_load(coreos_template)
60+
configuration_azure_template = yaml.safe_load(azure_template)
61+
62+
configuration = combine_dicts(configuration_coreos_template,
63+
configuration_azure_template)
64+
65+
configuration["coreos"]["etcd"]["discovery"] = url
66+
67+
for config in configuration['write_files']:
68+
if "DOCKER_OPTS" in config['content']:
69+
config['content'] = '[Service]\nEnvironment="DOCKER_OPTS=--insecure-registry 0.0.0.0/0"\n'
70+
71+
with azure_user_data as outfile:
72+
try:
73+
outfile.write("#cloud-config\n\n" + yaml.dump(configuration, default_flow_style=False))
74+
except (IOError, ValueError):
75+
print "There was an issue writing to file " + azure_user_data.name
76+
return 1
77+
else:
78+
print "Wrote file \"%s\" with url \"%s\"" %\
79+
(azure_user_data.name, url)
80+
81+
if __name__ == "__main__":
82+
sys.exit(main())
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
# generate management cert
4+
5+
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -config cert.conf -keyout azure-cert.pem -out azure-cert.pem -config cert.conf
6+
openssl x509 -outform der -in azure-cert.pem -out azure-cert.cer

0 commit comments

Comments
 (0)