from gzip import compress from urllib.parse import quote from pkg_resources import resource_string from json import loads, dumps from collections import OrderedDict def res_plain(resource): """Returns package data as plain bytes""" return resource_string(__name__, resource) def res_gzip(resource): """Returns package data as gzipped bytes""" return compress(res_plain(resource)) # Reusable data from static files files = { # systemd units 'coremeta' : res_plain('data/systemd/coreos-metadata.service.d/10-provider.conf'), 'coremetassh' : res_plain('data/systemd/coreos-metadata-sshkeys@.service.d/10-provider.conf'), 'kubelet' : res_plain('data/systemd/kubelet.service'), 'etcd' : res_plain('data/systemd/etcd-member.service.d/10-daemon.conf'), 'docker' : res_plain('data/systemd/docker.service.d/10-daemon.conf'), # k8s components manifests 'apiserver' : res_plain('data/k8s/manifests/kube-apiserver.json'), 'proxy' : res_plain('data/k8s/manifests/kube-proxy.json'), 'controller-manager' : res_plain('data/k8s/manifests/kube-controller-manager.json'), 'scheduler' : res_plain('data/k8s/manifests/kube-scheduler.json'), 'addon-manager' : res_gzip('data/k8s/manifests/kube-addon-manager.yml'), # k8s components config 'kubelet-config' : res_gzip('data/k8s/kubeletconfig.json'), 'proxy-config' : res_gzip('data/k8s/kubeproxyconfig.json'), 'controller-manager-config' : res_gzip('data/k8s/kubecontrollermanagerconfig.json'), 'scheduler-config' : res_gzip('data/k8s/kubeschedulerconfig.json'), # k8s addons manifests 'kubedns' : res_gzip('data/k8s/addons/kubedns.yml'), 'flannel' : res_gzip('data/k8s/addons/flannel.yml'), # k8s kubeconfig 'kubeconfig' : res_plain('data/k8s/kubeconfig.json') } class UserData: def __init__(self, k8s_ver='1.12.2'): self.k8s_ver = k8s_ver # boilerplate ignition config self.data = { 'ignition': { 'version': '2.1.0' }, 'storage': {}, 'systemd': {} } def add_files(self, definition): """Add elements to node storage['files']""" if not 'files' in self.data['storage']: self.data['storage']['files'] = [] if isinstance(definition, list): self.data['storage']['files'].extend(definition) else: raise TypeError("'definition must be a list, not '{}'".format(type(definition))) def add_sunits(self, definition): """Add elements to node systemd['units']""" if not 'units' in self.data['systemd']: self.data['systemd']['units'] = [] if isinstance(definition, list): self.data['systemd']['units'].extend(definition) else: raise TypeError("'definition must be a list, not '{}'".format(type(definition))) def configure_clinux_core(self): """Generate drop-ins for Container Linux core services""" self.add_sunits([ { 'name': 'coreos-metadata.service', 'dropins': [{ 'name': '10-provider.conf', 'contents': files['coremeta'].decode() }] }, { 'name': 'coreos-metadata-sshkeys@.service', 'enable': True, 'dropins': [{ 'name': '10-provider.conf', 'contents': files['coremetassh'].decode() }] }, { 'name': 'locksmithd.service', 'mask': True } ]) def gen_kubeconfig(self, component, server='localhost'): """Generate kubeconfig""" kubeconfig = loads(files['kubeconfig'].decode(), object_pairs_hook=OrderedDict) kubeconfig['users'][0]['user']['client-certificate'] = 'tls/client/{}.crt'.format(component) kubeconfig['clusters'][0]['cluster']['server'] = 'https://' + server + ':6443' kubeconfig = compress((dumps(kubeconfig, indent=2) + '\n').encode()) self.add_files([ { 'filesystem': 'root', 'path': '/etc/kubernetes/kubeconfig-' + component + '.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(kubeconfig) } } ]) def gen_kubemanifest(self, component, tag): """Generate Kubernetes Pod manifest""" manifest = loads(files[component].decode(), object_pairs_hook=OrderedDict) manifest['spec']['containers'][0]['image'] = 'k8s.gcr.io/hyperkube:v{}'.format(self.k8s_ver) manifest = compress((dumps(manifest, indent=2) + '\n').encode()) self.add_files([ { 'filesystem': 'root', 'path': '/etc/kubernetes/manifests/kube-{}.json'.format(component) + '.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(manifest) } } ]) def gen_kubelet_unit(self, roles): """Generate kubelet service unit""" labels = ("node-role.kubernetes.io/{}=''".format(r) for r in roles) self.add_sunits([ { 'name': 'kubelet.service', 'enable': True, 'contents': ( files['kubelet'].decode() .replace('__IMAGE_TAG__', 'v{}'.format(self.k8s_ver)) .replace('__NODE_LABELS__', ','.join(labels))) } ]) def gen_etc_hosts(self, client, net): """Generate /etc/hosts file containing all subnet hosts Makes it possible to register k8s nodes by hostname. Disgusting hack to make up for OVH's terrible DNS. """ from ipaddress import IPv4Network subnet = client.get('/cloud/project/{}/network/private/{}/subnet'.format(client._project, net))[0] hosts = IPv4Network(subnet['cidr']).hosts() hosts_content = ('127.0.0.1\tlocalhost\n' + '::1\t\tlocalhost\n' + '\n'.join(['{}\t{}'.format(ip, 'host-'+str(ip).replace('.', '-')) for ip in hosts]) + '\n').encode() self.add_files([ { 'filesystem': 'root', 'path': '/etc/hosts', 'mode': 420, # 0644 'contents': { 'source': 'data:,' + quote(hosts_content) } } ]) def gen_kube_data(self, roles): """Generate data deployed to all Kubernetes instances""" self.gen_kubelet_unit(roles) self.gen_kubemanifest('proxy', 'v{}'.format(self.k8s_ver)) self.add_files([ { 'filesystem': 'root', 'path': '/etc/kubernetes/kubeletconfig.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(files['kubelet-config']) } }, { 'filesystem': 'root', 'path': '/etc/kubernetes/kubeproxyconfig.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(files['proxy-config']) } } ]) # configure Docker daemon self.add_sunits([ { 'name': 'docker.service', 'dropins': [{ 'name': '10-daemon.conf', 'contents': files['docker'].decode() }] } ]) def gen_kubemaster_data(self): """Generate data deployed to all Kubernetes masters""" self.add_sunits([ { 'name': 'etcd-member.service', 'enable': True, 'dropins': [{ 'name': '10-daemon.conf', 'contents': files['etcd'].decode() }] } ]) for component in 'apiserver', 'scheduler', 'controller-manager': self.gen_kubemanifest(component, 'v{}'.format(self.k8s_ver)) self.add_files([ { 'filesystem': 'root', 'path': '/etc/kubernetes/kubecontrollermanagerconfig.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(files['controller-manager-config']) } }, { 'filesystem': 'root', 'path': '/etc/kubernetes/kubeschedulerconfig.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(files['scheduler-config']) } }, { 'filesystem': 'root', 'path': '/etc/kubernetes/manifests/kube-addon-manager.yml' + '.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(files['addon-manager']) } }, { 'filesystem': 'root', 'path': '/etc/kubernetes/addons/kubedns.yml' + '.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(files['kubedns']) } }, { 'filesystem': 'root', 'path': '/etc/kubernetes/addons/flannel.yml' + '.gz', 'mode': 416, # 0640 'contents': { 'source': 'data:,' + quote(files['flannel']) } }, { 'filesystem': 'root', 'path': '/opt/bin/kubectl', 'mode': 493, # 0755 'contents': { 'source': ('https://storage.googleapis.com/kubernetes-release/release/' 'v{}/bin/linux/amd64/kubectl').format(self.k8s_ver) } } ])