├── .gitignore
├── LICENSE
├── README.md
├── charts
└── seedbox
│ ├── Chart.yaml
│ ├── templates
│ ├── postgres
│ │ ├── deployment.yaml
│ │ ├── pvc.yaml
│ │ └── service.yaml
│ └── seedbox
│ │ ├── deployment.yaml
│ │ └── service.yaml
│ └── values.yaml
├── docker-compose.yml
├── docs
└── img
│ ├── cluster-create.png
│ ├── cluster-list.png
│ ├── node-create.png
│ ├── node-list.png
│ └── user-list.png
├── scripts
├── compare-ignition.py
├── unpack_ingnition.py
└── vbox_cluster.py
└── src
├── Dockerfile
├── requirements.txt
└── seedbox
├── __init__.py
├── __main__.py
├── admin
├── __init__.py
├── base.py
├── cluster.py
├── credentials_data.py
├── disk.py
├── node.py
├── provision.py
├── templates
│ └── admin
│ │ ├── cluster_details.html
│ │ ├── cluster_list.html
│ │ ├── cluster_macros.html
│ │ ├── credentials_details.html
│ │ ├── credentials_list.html
│ │ ├── credentials_macros.html
│ │ ├── index.html
│ │ ├── node_details.html
│ │ ├── node_list.html
│ │ ├── node_macros.html
│ │ ├── provision_details.html
│ │ ├── provision_list.html
│ │ ├── provision_macros.html
│ │ ├── user_details.html
│ │ ├── user_list.html
│ │ └── user_macros.html
└── user.py
├── app.py
├── config.py
├── config_renderer
├── __init__.py
├── charts.py
├── ignition
│ ├── __init__.py
│ ├── base.py
│ ├── credentials.py
│ ├── dnsmasq
│ │ ├── __init__.py
│ │ ├── dnsmasq.service
│ │ └── resolved.conf
│ ├── etcd_server
│ │ ├── __init__.py
│ │ └── etcd-member.service.d
│ │ │ ├── 30-image.conf
│ │ │ ├── 40-etcd-cluster.conf
│ │ │ ├── 40-oom.conf
│ │ │ ├── 40-persistent.conf
│ │ │ └── 40-ssl.conf
│ ├── flannel
│ │ ├── __init__.py
│ │ ├── cni-conf.json
│ │ └── flanneld.service.d
│ │ │ ├── 30-proxy.conf
│ │ │ ├── 40-etcd-cluster.conf
│ │ │ ├── 40-iface.conf
│ │ │ ├── 40-network-config.conf
│ │ │ └── 40-oom.conf
│ ├── k8s_master_manifests
│ │ ├── __init__.py
│ │ ├── kube-apiserver.yaml
│ │ ├── kube-controller-manager.yaml
│ │ └── kube-scheduler.yaml
│ ├── kube_proxy
│ │ ├── __init__.py
│ │ └── kube-proxy.yaml
│ ├── kubeconfig
│ │ ├── __init__.py
│ │ └── kubeconfig.yaml
│ ├── kubelet
│ │ ├── __init__.py
│ │ ├── kubelet.service
│ │ └── kubelet.service.d
│ │ │ └── 30-proxy.conf
│ └── system
│ │ ├── __init__.py
│ │ ├── add-http-proxy-ca-certificate.service
│ │ ├── addresses.network
│ │ ├── cluster-etcdctl
│ │ ├── containerd.service.d
│ │ └── 40-oom.conf
│ │ ├── docker.service.d
│ │ └── 30-proxy.conf
│ │ ├── fleet.service.d
│ │ └── 40-etcd-cluster.conf
│ │ ├── locksmithd.service.d
│ │ ├── 40-etcd-cluster.conf
│ │ └── 40-etcd-lock.conf
│ │ ├── provision-report.service
│ │ ├── sshd.service.d
│ │ └── 40-oom.conf
│ │ ├── sshd@.service.d
│ │ └── 40-oom.conf
│ │ ├── sysctl-max-user-watches.conf
│ │ └── volume.mount
├── ipxe.py
└── kubeconfig.py
├── exceptions.py
├── ignition_parser.py
├── manage.py
├── migrations
├── README
├── __init__.py
├── alembic.ini
├── env.py
├── script.py.mako
└── versions
│ ├── 028018cd8818_.py
│ ├── 03314dc2f789_.py
│ ├── 2093be28531b_.py
│ ├── 235f683efac0_.py
│ ├── 25fc3f52af44_.py
│ ├── 283dc9256301_.py
│ ├── 35099fc974d2_.py
│ ├── 450edaa5f10d_.py
│ ├── 469154bafe22_.py
│ ├── 519c11fc090d_.py
│ ├── 52e82e7e9376_.py
│ ├── 579f89f6b1a0_.py
│ ├── 635f6467bb33_.py
│ ├── 91248bf831b8_.py
│ ├── 92f518191c12_.py
│ ├── 9a65898a0e2f_.py
│ ├── __init__.py
│ ├── aa55bedebb6f_.py
│ ├── ae00e7974dca_.py
│ ├── b70159a8c7c2_.py
│ ├── bf3ac4dda2ab_.py
│ ├── da3084151a1d_.py
│ └── f08e29e85dc0_.py
├── models
├── __init__.py
├── address.py
├── cluster.py
├── credentials_data.py
├── db.py
├── disk.py
├── disk_partition.py
├── mountpoint.py
├── node.py
├── provision.py
└── user.py
├── pki.py
├── update_watcher.py
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | /.idea/
3 | /test.db
4 | /tmp
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2017 Dmitry Bashkatov
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # seedbox
2 |
3 | Baremetal CoreOS cluster provisioner with web UI. Currently its primary goal is to boot CoreOS
4 | clusters using PXE without installation. But it easily can be extended to render kubernetes assets for
5 | different deployments.
6 |
7 |
8 | ## Installation
9 |
10 | ```
11 | $ docker-compose up
12 | $ docker-compose exec seedbox python -m seedbox db upgrade
13 | ```
14 |
15 | Open http://localhost:5000/admin/
16 |
17 |
18 | ### helm chart
19 |
20 | If you have already running Kubernetes cluster and want to provision another one, take a took
21 | at [seedbox helm chart](charts/seedbox).
22 |
23 |
24 | ## Comparison with other projects
25 |
26 | * [CoreOS matchbox](https://github.com/coreos/matchbox). It doesn't provide complete config rendering facility.
27 | There are examples, but they are hard to read and not modularized.
28 |
29 | * [Tectonic installer](https://github.com/coreos/tectonic-installer). This is missing config rendering app
30 | for matchbox. This project was open-sourced after seedbox was started. It's highly modularized
31 | and uses HCL (HashiCorp configuration language) to render config files. But it's tied to CoreOS tectonic.
32 | There is no option to render clean Kubernetes configuration.
33 |
34 | Seedbox is all-in-one project compared to those above. You will get web UI, iPXE handler, config renderer, PKI,
35 | cluster state tracking and maybe something more in future versions.
36 |
37 | Actually seedbox config template files are based on files rendered by **closed**-source tectonic installer.
38 | They are split into packages by function and rendered using Python Jinja2 template engine.
39 |
40 | **Closed**-source Tectonic installer is claimed as installer for production grade clusters. So you can
41 | say this about seedbox as well. 😀 But I'm not sure of that.
42 |
43 |
44 | ## Web UI
45 |
46 | 
47 |
48 | 
49 |
50 | 
51 |
52 | 
53 |
54 | 
55 |
56 |
57 | ## PKI
58 |
59 | You will have PKI out of the box. It's simple but powerful enough. It provides one CA per cluster and will
60 | automatically issue certificates for nodes and users. Also it will warn you if there is something
61 | wrong with certificates (expired, changed name, etc).
62 |
63 | Credentials are automatically transferred to nodes in most secure manner possible for automatic provision.
64 |
65 |
66 | ## Node state tracking
67 |
68 | Nodes notify seedbox after successful boot and upload active ignition config, so seedbox can track
69 | current state of a cluster.
70 |
71 |
72 | ## Toubleshooting
73 |
74 | Due to some CoreOS bug host hangs on boot without any error messages if root partition doesn't exist. If this happens to
75 | you, just set "Wipe root disk on next boot" checkbox in node admin.
76 |
--------------------------------------------------------------------------------
/charts/seedbox/Chart.yaml:
--------------------------------------------------------------------------------
1 | description: Baremetal CoreOS cluster provisioner with web UI
2 | name: seedbox
3 | version: 1.0.0
4 |
--------------------------------------------------------------------------------
/charts/seedbox/templates/postgres/deployment.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: extensions/v1beta1
3 | kind: Deployment
4 | metadata:
5 | labels:
6 | app: postgres
7 | release: {{ .Release.Name }}
8 | version: {{ .Chart.Version }}
9 | name: postgres
10 | spec:
11 | replicas: 1
12 | template:
13 | metadata:
14 | name: postgres
15 | labels:
16 | app: postgres
17 | release: {{ .Release.Name }}
18 | version: {{ .Chart.Version }}
19 | spec:
20 | volumes:
21 | - name: postgres
22 | persistentVolumeClaim:
23 | claimName: postgres
24 | containers:
25 | - name: main
26 | image: postgres:9.6
27 | ports:
28 | - containerPort: 5432
29 | env:
30 | - name: POSTGRES_USER
31 | value: seedbox
32 | volumeMounts:
33 | - name: postgres
34 | mountPath: /var/lib/postgresql/data
35 | subPath: data
36 |
--------------------------------------------------------------------------------
/charts/seedbox/templates/postgres/pvc.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | kind: PersistentVolumeClaim
3 | apiVersion: v1
4 | metadata:
5 | name: postgres
6 | spec:
7 | accessModes: [ "ReadWriteOnce" ]
8 | resources:
9 | requests:
10 | storage: 256Mi
11 |
--------------------------------------------------------------------------------
/charts/seedbox/templates/postgres/service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: postgres
6 | labels:
7 | app: postgres
8 | release: {{ .Release.Name }}
9 | spec:
10 | ports:
11 | - port: 5432
12 | protocol: TCP
13 | targetPort: 5432
14 | selector:
15 | app: postgres
16 | release: {{ .Release.Name }}
17 |
--------------------------------------------------------------------------------
/charts/seedbox/templates/seedbox/deployment.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: extensions/v1beta1
3 | kind: Deployment
4 | metadata:
5 | labels:
6 | app: seedbox
7 | release: {{ .Release.Name }}
8 | version: {{ .Chart.Version }}
9 | name: seedbox
10 | spec:
11 | replicas: 1
12 | template:
13 | metadata:
14 | name: seedbox
15 | labels:
16 | app: seedbox
17 | release: {{ .Release.Name }}
18 | version: {{ .Chart.Version }}
19 | spec:
20 | containers:
21 | - name: web
22 | image: {{ .Values.image }}
23 | args: [runserver, --host, 0.0.0.0]
24 | ports:
25 | - containerPort: 5000
26 | env:
27 | - name: SECRET_KEY
28 | value: {{ .Values.secret_key | quote }}
29 | - name: ALLOW_INSECURE_TRANSPORT
30 | value: {{ .Values.allow_insecure_transport | quote }}
31 | - name: ADMIN_PASSWORD
32 | value: {{ .Values.admin_password | quote }}
33 | - name: DATABASE_URI
34 | value: postgres://seedbox@postgres/seedbox
35 | - name: REVERSE_PROXY_COUNT
36 | value: {{ .Values.reverse_proxy_count | quote }}
37 | - name: UPDATE_STATE_FILE
38 | value: /seedbox/versions.json
39 | volumeMounts:
40 | - name: state
41 | mountPath: /seedbox
42 | - name: update-watcher
43 | image: {{ .Values.image }}
44 | args: [watch_updates]
45 | env:
46 | - name: UPDATE_STATE_FILE
47 | value: /seedbox/versions.json
48 | volumeMounts:
49 | - name: state
50 | mountPath: /seedbox
51 | volumes:
52 | - name: state
53 | emptyDir: {}
54 |
--------------------------------------------------------------------------------
/charts/seedbox/templates/seedbox/service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: web
6 | labels:
7 | app: seedbox
8 | release: {{ .Release.Name }}
9 | spec:
10 | ports:
11 | - port: 80
12 | protocol: TCP
13 | targetPort: 5000
14 | selector:
15 | app: seedbox
16 | release: {{ .Release.Name }}
17 |
--------------------------------------------------------------------------------
/charts/seedbox/values.yaml:
--------------------------------------------------------------------------------
1 | image: nailgun/seedbox
2 | secret_key: 'my secret key'
3 | allow_insecure_transport: 'false'
4 | admin_password: admin
5 | reverse_proxy_count: 0
6 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: '2'
3 |
4 | services:
5 | seedbox:
6 | image: seedbox
7 | build: ./src/
8 | command: runserver --host 0.0.0.0 --port 5000 --debug
9 | environment:
10 | - DATABASE_URI=postgres://seedbox:password@postgres:5432/seedbox
11 | ports:
12 | - '5000:5000'
13 | volumes:
14 | - './src:/usr/src/app'
15 | depends_on:
16 | - postgres
17 |
18 | postgres:
19 | image: postgres:9.6
20 | ports:
21 | - '5432:5432'
22 | environment:
23 | - POSTGRES_USER=seedbox
24 |
--------------------------------------------------------------------------------
/docs/img/cluster-create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/docs/img/cluster-create.png
--------------------------------------------------------------------------------
/docs/img/cluster-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/docs/img/cluster-list.png
--------------------------------------------------------------------------------
/docs/img/node-create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/docs/img/node-create.png
--------------------------------------------------------------------------------
/docs/img/node-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/docs/img/node-list.png
--------------------------------------------------------------------------------
/docs/img/user-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/docs/img/user-list.png
--------------------------------------------------------------------------------
/scripts/compare-ignition.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import json
4 | import difflib
5 | import argparse
6 | import urllib.parse
7 | from collections import defaultdict
8 |
9 | not_set = object()
10 |
11 |
12 | def main():
13 | parser = argparse.ArgumentParser()
14 | parser.add_argument('v1')
15 | parser.add_argument('v2')
16 | args = parser.parse_args()
17 |
18 | with open(args.v1, 'rb') as v1:
19 | v1 = json.load(v1)
20 |
21 | with open(args.v2, 'rb') as v2:
22 | v2 = json.load(v2)
23 |
24 | compare('', v1, v2)
25 |
26 |
27 | def compare(path, v1, v2):
28 | if v1 is not_set:
29 | return print('+', path, '\n\n')
30 |
31 | if v2 is not_set:
32 | return print('-', path, '\n\n')
33 |
34 | if type(v1) != type(v2):
35 | print('-', path, 'type:', type(v1))
36 | print('+', path, 'type:', type(v1))
37 | return print('\n')
38 |
39 | if path == '/storage/files':
40 | v1src = v1
41 | v1 = defaultdict(list)
42 | for f in v1src:
43 | v1['[{filesystem}:{path}]'.format(**f)].append(f)
44 | v2src = v2
45 | v2 = defaultdict(list)
46 | for f in v2src:
47 | v2['[{filesystem}:{path}]'.format(**f)].append(f)
48 |
49 | if path == '/systemd/units':
50 | v1src = v1
51 | v1 = defaultdict(list)
52 | for u in v1src:
53 | v1['[{name}]'.format(**u)].append(u)
54 | v2src = v2
55 | v2 = defaultdict(list)
56 | for u in v2src:
57 | v2['[{name}]'.format(**u)].append(u)
58 |
59 | if isinstance(v1, dict):
60 | return compare_dict(path, v1, v2)
61 |
62 | if isinstance(v1, list):
63 | return compare_list(path, v1, v2)
64 |
65 | if isinstance(v1, str) and v1.startswith('data:,'):
66 | v1 = v1[len('data:,'):]
67 | v1 = urllib.parse.unquote(v1)
68 |
69 | if isinstance(v2, str) and v2.startswith('data:,'):
70 | v2 = v2[len('data:,'):]
71 | v2 = urllib.parse.unquote(v2)
72 |
73 | if v1 != v2:
74 | if isinstance(v1, str):
75 | diff = difflib.unified_diff(v1.splitlines(keepends=True), v2.splitlines(keepends=True))
76 | delta = ''.join(x for x in diff)
77 | print(path, ':')
78 | print(delta, '\n')
79 | else:
80 | print(path, ':')
81 | print('-', v1)
82 | print('+', v2, '\n\n')
83 |
84 |
85 | def compare_dict(path, v1, v2):
86 | keys = set(v1.keys()) | set(v2.keys())
87 | keys = sorted(keys)
88 |
89 | for k in keys:
90 | v1i = v1.get(k, not_set)
91 | v2i = v2.get(k, not_set)
92 | compare(path + '/' + k, v1i, v2i)
93 |
94 |
95 | def compare_list(path, v1, v2):
96 | max_len = max(len(v1), len(v2))
97 |
98 | for idx in range(max_len):
99 | try:
100 | v1i = v1[idx]
101 | except IndexError:
102 | v1i = not_set
103 | try:
104 | v2i = v2[idx]
105 | except IndexError:
106 | v2i = not_set
107 | compare(path + '/' + str(idx), v1i, v2i)
108 |
109 |
110 | if __name__ == '__main__':
111 | main()
112 |
--------------------------------------------------------------------------------
/scripts/unpack_ingnition.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import json
5 | import shutil
6 | import argparse
7 |
8 | from seedbox import ignition_parser
9 |
10 |
11 | def main():
12 | arg_parser = argparse.ArgumentParser()
13 | arg_parser.add_argument('file')
14 | arg_parser.add_argument('dir')
15 | args = arg_parser.parse_args()
16 |
17 | with open(args.file, 'rb') as f:
18 | ignition = json.load(f)
19 |
20 | if os.path.exists(args.dir):
21 | shutil.rmtree(args.dir)
22 |
23 | for path, data in ignition_parser.iter_files(ignition):
24 | out_path = os.path.join(args.dir, path)
25 | os.makedirs(os.path.dirname(out_path), exist_ok=True)
26 | with open(out_path, 'wb') as f:
27 | f.write(data)
28 |
29 |
30 | if __name__ == '__main__':
31 | main()
32 |
--------------------------------------------------------------------------------
/scripts/vbox_cluster.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 | import time
6 | import logging
7 | import argparse
8 | import requests
9 | import subprocess
10 | import urllib.parse
11 |
12 |
13 | log = logging.getLogger('main')
14 | tmp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'tmp')
15 | ipxe_iso = os.path.join(tmp_dir, 'ipxe.iso')
16 |
17 | if sys.platform == 'darwin':
18 | vbox_tftp_dir = os.path.expanduser('~/Library/VirtualBox/TFTP')
19 | else:
20 | vbox_tftp_dir = os.path.expanduser('~/.VirtualBox/TFTP')
21 |
22 |
23 | def main():
24 | args = parse_args()
25 | logging.basicConfig(level='NOTSET')
26 |
27 | seedbox_url = args.seedbox_url
28 | if not seedbox_url:
29 | seedbox_ip = get_hostonlyif_ipaddress(args.host_only_network)
30 | if not seedbox_ip:
31 | raise Exception("Can't get host-only network IP", args.host_only_network)
32 | seedbox_url = 'http://{}:5000/'.format(seedbox_ip)
33 |
34 | ipxe_script_name = args.cluster_name + '.ipxe'
35 | prepare_vbox_ipxe(ipxe_script_name, seedbox_url)
36 |
37 | instance_names = [format_instance_name(args.cluster_name, idx + 1) for idx in range(args.num_instances)]
38 |
39 | if args.delete:
40 | wait = False
41 | for instance_name in instance_names:
42 | log.info('Powering off VM %s', instance_name)
43 | poweroff_cmd = subprocess.run(['VBoxManage', 'controlvm',
44 | instance_name,
45 | 'poweroff'])
46 | if poweroff_cmd.returncode == 0:
47 | wait = True # VirtualBox doesn't release a lock for some time after poweroff
48 |
49 | if wait:
50 | time.sleep(10)
51 |
52 | for instance_name in instance_names:
53 | log.info('Deleting VM %s', instance_name)
54 | subprocess.run(['VBoxManage', 'unregistervm',
55 | instance_name,
56 | '--delete'])
57 |
58 | for instance_name in instance_names:
59 | log.info('Creating VM %s', instance_name)
60 | subprocess.run(['VBoxManage', 'createvm',
61 | '--name', instance_name,
62 | '--groups', '/' + args.cluster_name,
63 | '--ostype', 'Linux26_64',
64 | '--register'],
65 | check=True)
66 |
67 | log.info('Configuring VM %s', instance_name)
68 | subprocess.run(['VBoxManage', 'modifyvm',
69 | instance_name,
70 | '--memory', str(args.memory),
71 | '--nic1', 'nat',
72 | '--nattftpfile1', ipxe_script_name,
73 | '--nic2', 'hostonly',
74 | '--hostonlyadapter2', args.host_only_network],
75 | check=True)
76 |
77 | config_path = get_vm_config_file_path(instance_name)
78 | if not config_path:
79 | raise Exception("Can't get VM config path", instance_name)
80 |
81 | base_path = os.path.dirname(config_path)
82 | disk_path = os.path.join(base_path, 'ide1.vdi')
83 |
84 | log.info('Creating a disk for VM %s', instance_name)
85 | subprocess.run(['VBoxManage', 'createhd',
86 | '--filename', disk_path,
87 | '--size', str(args.disk_size)],
88 | check=True)
89 |
90 | storage_controller_name = 'IDE Controller'
91 |
92 | log.info('Attaching IDE controller to VM %s', instance_name)
93 | subprocess.run(['VBoxManage', 'storagectl',
94 | instance_name,
95 | '--name', storage_controller_name,
96 | '--add', 'ide',
97 | '--controller', 'PIIX4'],
98 | check=True)
99 |
100 | log.info('Attaching disk to VM %s', instance_name)
101 | subprocess.run(['VBoxManage', 'storageattach',
102 | instance_name,
103 | '--storagectl', storage_controller_name,
104 | '--port', '0',
105 | '--device', '0',
106 | '--type', 'hdd',
107 | '--medium', disk_path],
108 | check=True)
109 |
110 | log.info('Attaching ipxe.iso to VM %s', instance_name)
111 | subprocess.run(['VBoxManage', 'storageattach',
112 | instance_name,
113 | '--storagectl', storage_controller_name,
114 | '--port', '0',
115 | '--device', '1',
116 | '--type', 'dvddrive',
117 | '--medium', ipxe_iso],
118 | check=True)
119 |
120 | if args.start:
121 | for instance_name in instance_names:
122 | log.info('Starting VM %s', instance_name)
123 | subprocess.run(['VBoxManage', 'startvm',
124 | instance_name,
125 | '--type', 'headless'],
126 | check=True)
127 |
128 |
129 | def parse_args():
130 | arg_parser = argparse.ArgumentParser()
131 | arg_parser.add_argument('cluster_name',
132 | help='cluster name')
133 | arg_parser.add_argument('num_instances', type=int,
134 | help='number of instances to create')
135 | arg_parser.add_argument('-d', '--delete', action='store_true', default=False,
136 | help='delete all VMs first')
137 | arg_parser.add_argument('--memory', type=int, default=1024,
138 | help='amount of RAM on an instance')
139 | arg_parser.add_argument('--disk-size', type=int, default=262144,
140 | help='instance disk size in megabytes')
141 | arg_parser.add_argument('--start', action='store_true', default=False,
142 | help='start instances after creation')
143 | arg_parser.add_argument('--host-only-network', default='vboxnet0',
144 | help='VirtualBox host-only network name')
145 | arg_parser.add_argument('--seedbox-url',
146 | help='URL of your seedbox installation (defaults to first IP of host-only network)')
147 | return arg_parser.parse_args()
148 |
149 |
150 | def format_instance_name(cluster_name, idx):
151 | return '{}-node{:02}'.format(cluster_name, idx)
152 |
153 |
154 | def prepare_vbox_ipxe(ipxe_script_name, seedbox_url):
155 | try:
156 | os.mkdir(vbox_tftp_dir)
157 | except FileExistsError:
158 | pass
159 |
160 | ipxe_script_path = os.path.join(vbox_tftp_dir, ipxe_script_name)
161 | ipxe_script = '#!ipxe\ndhcp net1\nchain {}\n'.format(urllib.parse.urljoin(seedbox_url, 'ipxe'))
162 | with open(ipxe_script_path, 'w') as f:
163 | f.write(ipxe_script)
164 |
165 | if not os.path.exists(ipxe_iso):
166 | os.makedirs(tmp_dir, exist_ok=True)
167 | log.info('Downloading ipxe.iso')
168 | source_url = 'http://boot.ipxe.org/ipxe.iso'
169 | resp = requests.get(source_url, stream=True)
170 | resp.raise_for_status()
171 | with open(ipxe_iso, 'wb') as f:
172 | for chunk in resp.iter_content(chunk_size=1024):
173 | f.write(chunk)
174 |
175 |
176 | def get_vm_config_file_path(vm_name):
177 | stdout = subprocess.run(['VBoxManage', 'showvminfo', vm_name], check=True, stdout=subprocess.PIPE).stdout
178 | for line in stdout.splitlines():
179 | parts = line.split(b':', maxsplit=1)
180 | if len(parts) == 2 and parts[0] == b'Config file':
181 | return os.fsdecode(parts[1].strip())
182 |
183 |
184 | def get_hostonlyif_ipaddress(ifname):
185 | stdout = subprocess.run(['VBoxManage', 'list', 'hostonlyifs'], check=True, stdout=subprocess.PIPE).stdout
186 |
187 | current_if = None
188 | for line in stdout.splitlines():
189 | if not line:
190 | current_if = None
191 | else:
192 | parts = line.split(b':', maxsplit=1)
193 | key = parts[0]
194 | value = parts[1].strip()
195 | if key == b'Name':
196 | current_if = value.decode()
197 | elif current_if == ifname:
198 | if key == b'IPAddress':
199 | return value.decode()
200 |
201 |
202 | if __name__ == '__main__':
203 | main()
204 |
--------------------------------------------------------------------------------
/src/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6-onbuild
2 |
3 | ENTRYPOINT ["python", "-m", "seedbox"]
4 | CMD ["runserver"]
5 |
--------------------------------------------------------------------------------
/src/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask~=0.12.1
2 | Flask-Admin~=1.5.0
3 | Flask-SQLAlchemy~=2.2
4 | Flask-Migrate~=2.0.3
5 | Flask-Script~=2.0.5
6 | cryptography~=1.8.1
7 | pyOpenSSL~=16.2.0
8 | psycopg2~=2.7.1
9 | PyYAML~=3.12
10 | requests~=2.13.0
11 |
--------------------------------------------------------------------------------
/src/seedbox/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/src/seedbox/__init__.py
--------------------------------------------------------------------------------
/src/seedbox/__main__.py:
--------------------------------------------------------------------------------
1 | from seedbox import manage
2 |
3 |
4 | if __name__ == '__main__':
5 | manage.run()
6 |
--------------------------------------------------------------------------------
/src/seedbox/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_admin import Admin
2 |
3 | from seedbox import models, config
4 | from .cluster import ClusterView
5 | from .node import NodeView
6 | from .user import UserView
7 | from .credentials_data import CredentialsDataView
8 | from .provision import ProvisionView
9 | from .disk import DiskView
10 |
11 |
12 | admin = Admin(name='seedbox', url=config.admin_base_url, template_mode='bootstrap3')
13 | admin.add_view(ClusterView(models.Cluster, models.db.session))
14 | admin.add_view(NodeView(models.Node, models.db.session))
15 | admin.add_view(DiskView(models.Disk, models.db.session))
16 | admin.add_view(UserView(models.User, models.db.session))
17 | admin.add_view(CredentialsDataView(models.CredentialsData, models.db.session))
18 | admin.add_view(ProvisionView(models.Provision, models.db.session))
19 |
--------------------------------------------------------------------------------
/src/seedbox/admin/base.py:
--------------------------------------------------------------------------------
1 | from flask_admin.contrib.sqla import ModelView as BaseModelView
2 |
3 |
4 | class ModelView(BaseModelView):
5 | can_view_details = True
6 |
--------------------------------------------------------------------------------
/src/seedbox/admin/cluster.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 |
4 | from flask import request, flash, redirect
5 | from flask_admin import expose
6 | from flask_admin.form import rules
7 | from flask_admin.helpers import get_redirect_target
8 | from flask_admin.model.template import macro
9 |
10 | from seedbox import pki, models, config_renderer, config
11 | from .base import ModelView
12 |
13 | log = logging.getLogger()
14 |
15 |
16 | class ClusterView(ModelView):
17 | column_list = ['name', 'ca_credentials', 'is_configured', 'assert_etcd_cluster_exists', 'info']
18 | list_template = 'admin/cluster_list.html'
19 | details_template = 'admin/cluster_details.html'
20 | form_excluded_columns = [
21 | 'ca_credentials',
22 | 'nodes',
23 | 'users',
24 | 'k8s_service_account_public_key',
25 | 'k8s_service_account_private_key',
26 | ]
27 | column_formatters = {
28 | 'ca_credentials': macro('render_ca_credentials'),
29 | 'info': macro('render_info'),
30 | }
31 | column_labels = {
32 | 'ca_credentials': "CA credentials",
33 | 'etcd_image_tag': "etcd image tag",
34 | 'assert_etcd_cluster_exists': "Assert etcd cluster already exists",
35 | 'etcd_nodes_dns_name': "DNS name of any etcd node",
36 | 'install_dnsmasq': "Install dnsmasq on cluster nodes",
37 | 'k8s_apiservers_audit_log': "Enable audit log on apiservers",
38 | 'k8s_apiservers_swagger_ui': "Enable Swagger-UI on apiservers",
39 | 'dnsmasq_static_records': "Add static records to dnsmasq",
40 | 'explicitly_advertise_addresses': "Explicitly advertise addresses",
41 | 'k8s_pod_network': "Pod network CIDR",
42 | 'k8s_service_network': "Service network CIDR",
43 | 'k8s_hyperkube_tag': "Hyperkube image tag",
44 | 'k8s_cni': "Use CNI",
45 | 'k8s_apiservers_dns_name': "DNS name of any master node",
46 | 'k8s_is_rbac_enabled': "Enable RBAC",
47 | 'k8s_admission_control': "Admission control",
48 | 'custom_coreos_images_base_url': "Custom base HTTP URL of CoreOS images",
49 | 'aci_proxy_url': "ACI proxy URL",
50 | 'aci_proxy_ca_cert': "ACI proxy CA certificate (PEM)",
51 | 'coreos_channel': "CoreOS channel",
52 | 'coreos_version': "CoreOS version",
53 | }
54 | column_descriptions = {
55 | 'name': "Human readable cluster name. Don't use spaces.",
56 | 'assert_etcd_cluster_exists': "This will set `initial-cluster-state` to `existing` for newly provisioned "
57 | "etcd members. Use it to add etcd members to existing cluster.",
58 | 'etcd_nodes_dns_name': "Must be round-robin DNS record. If this is set it will be used by "
59 | "all components to access etcd instead of hardcoded node list. You can "
60 | "add/remove nodes at any time just by updating DNS record.",
61 | 'install_dnsmasq': "If this is set, dnsmasq will be run on each node for resolving cluster.local zone "
62 | "using k8s DNS and for DNS caching.",
63 | 'dnsmasq_static_records': "Hosts' dnsmasq will serve cluster nodes' FQDNs and cluster components "
64 | "like etcd and apiserver.",
65 | 'explicitly_advertise_addresses': "If this is set, cluster components will explicitly advertise "
66 | "node IP as it set in seedbox.",
67 | 'k8s_apiservers_dns_name': "Must be round-robin DNS record. If this is set it will be used by "
68 | "all components to access apiserver instead of hardcoded node list. You can "
69 | "add/remove nodes at any time just by updating DNS record.",
70 | 'k8s_is_rbac_enabled': "Set kube-apiserver authentication mode to RBAC. Otherwise it will be AlwaysAllow.",
71 | 'k8s_admission_control': "Ordered list of plug-ins to do admission control of resources into cluster.",
72 | 'aci_proxy_url': "Docker and rkt will use this proxy to download container images.",
73 | 'aci_proxy_ca_cert': "Docker and rkt download images via HTTPS. If your proxy intercepts "
74 | "HTTPS connections you should add proxy CA certificate here. It will be "
75 | "added to system root CA certificates on each node.",
76 | 'custom_coreos_images_base_url': "coreos_production_pxe.vmlinuz and coreos_production_pxe_image.cpio.gz. It "
77 | "will be used instead of default if set.",
78 | 'coreos_channel': "stable, beta or alpha",
79 | 'docker_config': "~/.docker/config.json file with auth and other configuration."
80 | }
81 | form_rules = [
82 | rules.Field('name'),
83 | rules.Field('install_dnsmasq'),
84 | rules.Field('docker_config'),
85 | rules.FieldSet([
86 | 'etcd_image_tag',
87 | 'assert_etcd_cluster_exists',
88 | 'etcd_nodes_dns_name',
89 | ], 'etcd'),
90 | rules.FieldSet([
91 | 'k8s_apiservers_audit_log',
92 | 'k8s_apiservers_swagger_ui',
93 | 'k8s_pod_network',
94 | 'k8s_service_network',
95 | 'k8s_hyperkube_tag',
96 | 'k8s_cni',
97 | 'k8s_apiservers_dns_name',
98 | 'k8s_is_rbac_enabled',
99 | 'k8s_admission_control',
100 | ], 'Kubernetes'),
101 | rules.FieldSet([
102 | 'coreos_channel',
103 | 'coreos_version',
104 | 'custom_coreos_images_base_url',
105 | 'aci_proxy_url',
106 | 'aci_proxy_ca_cert',
107 | ], 'Images'),
108 | rules.FieldSet([
109 | 'dnsmasq_static_records',
110 | 'explicitly_advertise_addresses',
111 | ], 'Testing environment'),
112 | ]
113 |
114 | def _issue_creds(self, model):
115 | ca = models.CredentialsData()
116 | ca.cert, ca.key = pki.create_ca_certificate(model.name,
117 | certify_days=10000)
118 | self.session.add(ca)
119 | model.ca_credentials = ca
120 | model.k8s_service_account_public_key, model.k8s_service_account_private_key = pki.generate_rsa_keypair()
121 |
122 | def on_model_change(self, form, model, is_created):
123 | if is_created:
124 | self._issue_creds(model)
125 | else:
126 | model.nodes.update({models.Node.target_config_version: models.Node.target_config_version + 1})
127 |
128 | def after_model_delete(self, model):
129 | models.CredentialsData.query.filter_by(id=model.ca_credentials_id).delete()
130 |
131 | @expose('/reissue-ca-credentials', methods=['POST'])
132 | def reissue_ca_creds_view(self):
133 | model = self.get_one(request.args.get('id'))
134 | model.nodes.update({models.Node.target_config_version: models.Node.target_config_version + 1})
135 | self._issue_creds(model)
136 | self.session.add(model)
137 | self.session.commit()
138 | return_url = get_redirect_target() or self.get_url('.index_view')
139 | flash('The credentials successfully reissued', 'success')
140 | return redirect(return_url)
141 |
142 | @property
143 | def k8s_addons_dict(self):
144 | return config_renderer.charts.addons
145 |
146 | @expose('/reset-state', methods=['POST'])
147 | def reset_state(self):
148 | model = self.get_one(request.args.get('id'))
149 | model.nodes.update({
150 | models.Node.target_config_version: 1,
151 | models.Node.active_config_version: 0,
152 | })
153 | models.Disk.query.filter(models.Disk.node.has(cluster_id=model.id)).update({
154 | models.Disk.wipe_next_boot: True
155 | }, synchronize_session='fetch')
156 | model.assert_etcd_cluster_exists = False
157 | self.session.add(model)
158 | self.session.commit()
159 | return_url = get_redirect_target() or self.get_url('.index_view')
160 | flash('The cluster state has been successfully reset', 'success')
161 | return redirect(return_url)
162 |
163 | @property
164 | def latest_component_versions(self):
165 | try:
166 | with open(config.update_state_file, 'r') as fp:
167 | return json.load(fp)
168 | except Exception:
169 | log.exception('Failed to load component versions file')
170 | return None
171 |
--------------------------------------------------------------------------------
/src/seedbox/admin/credentials_data.py:
--------------------------------------------------------------------------------
1 | from flask import request, Response
2 | from flask_admin import expose
3 | from flask_admin.model.template import macro
4 |
5 | from seedbox import pki
6 | from .base import ModelView
7 |
8 |
9 | class CredentialsDataView(ModelView):
10 | column_list = ['cert', 'key']
11 | list_template = 'admin/credentials_list.html'
12 | details_template = 'admin/credentials_details.html'
13 | column_formatters = {
14 | 'cert': macro('render_cert'),
15 | 'key': macro('render_key'),
16 | }
17 |
18 | @expose('/cert.pem')
19 | def cert_view(self):
20 | creds = self.get_one(request.args.get('id'))
21 | return Response(creds.cert, mimetype='text/plain')
22 |
23 | @expose('/key.pem')
24 | def key_view(self):
25 | creds = self.get_one(request.args.get('id'))
26 | return Response(creds.key, mimetype='text/plain')
27 |
28 | @expose('/cert.txt')
29 | def cert_text_view(self):
30 | creds = self.get_one(request.args.get('id'))
31 | info = pki.get_certificate_text(creds.cert)
32 | return Response(info, mimetype='text/plain')
33 |
34 | def is_visible(self):
35 | return False
36 |
--------------------------------------------------------------------------------
/src/seedbox/admin/disk.py:
--------------------------------------------------------------------------------
1 | from seedbox import models
2 | from .base import ModelView
3 |
4 |
5 | class DiskView(ModelView):
6 | column_list = [
7 | 'node',
8 | 'device',
9 | 'wipe_next_boot',
10 | ]
11 |
12 | column_labels = {
13 | 'sector_size_bytes': "Size of a sector (bytes)",
14 | }
15 | column_descriptions = {
16 | 'device': "E.g. /dev/sda.",
17 | 'wipe_next_boot': "If this is set, disk partition table will be wiped on next boot.",
18 | 'sector_size_bytes': "Usually it equals 512.",
19 | }
20 | inline_models = [
21 | (models.DiskPartition, {
22 | 'column_labels': {
23 | 'size_mibs': "Size (MiB)",
24 | },
25 | 'column_descriptions': {
26 | 'number': "The partition number in the partition table (one - indexed).",
27 | 'label': "Label for partition and filesystem.",
28 | 'size_mibs': "If not set, the partition will fill the remainder of the disk.",
29 | 'format': "Filesystem format (ext4, btrfs, or xfs).",
30 | }
31 | }),
32 | ]
33 |
34 | def on_model_change(self, form, model, is_created):
35 | model.node.target_config_version += 1
36 | self.session.add(model.node)
37 |
--------------------------------------------------------------------------------
/src/seedbox/admin/node.py:
--------------------------------------------------------------------------------
1 | from flask import request, Response, flash, redirect
2 | from flask_admin import expose
3 | from flask_admin.form import rules
4 | from flask_admin.helpers import get_redirect_target
5 | from flask_admin.model.template import macro
6 |
7 | from seedbox import pki, models, config_renderer
8 | from .base import ModelView
9 |
10 |
11 | class NodeView(ModelView):
12 | column_list = [
13 | 'cluster',
14 | 'fqdn',
15 | 'ip',
16 | 'maintenance_mode',
17 | 'credentials',
18 | 'config',
19 | ]
20 | list_template = 'admin/node_list.html'
21 | details_template = 'admin/node_details.html'
22 | form_excluded_columns = [
23 | 'credentials',
24 | 'target_config_version',
25 | 'active_config_version',
26 | 'provisions',
27 | 'disks',
28 | ]
29 | column_formatters = {
30 | 'credentials': macro('render_credentials'),
31 | 'config': macro('render_config'),
32 | }
33 | column_labels = {
34 | 'ip': "Public IP",
35 | 'fqdn': "Fully Qualified Domain Name",
36 | 'maintenance_mode': "Maintenance mode",
37 | 'debug_boot': "Debug boot",
38 | 'coreos_autologin': "Enable terminal autologin",
39 | 'linux_consoles': "Linux console devices",
40 | 'disable_ipv6': "Disable IPv6 in Linux kernel",
41 | 'is_etcd_server': "etcd server",
42 | 'is_k8s_schedulable': "Kubernetes schedulable",
43 | 'is_k8s_master': "Kubernetes master",
44 | 'mountpoints': 'Additional mountpoints',
45 | 'addresses': 'Additional IP addresses',
46 | }
47 | column_descriptions = {
48 | 'maintenance_mode': "If this is enabled, node will be booted in minimal CoreOS environment without "
49 | "touching root partition.",
50 | 'debug_boot': "Forward all system journal messages to kmsg for troubleshooting.",
51 | 'coreos_autologin': "If this is set, main terminal will be logged-in with `core` user after boot. Useful "
52 | "for debugging. Don't enable in production.",
53 | 'linux_consoles': "Passed to kernel as `console` arguments. (Separate by comma.)",
54 | 'disable_ipv6': "Passed to kernel as `ipv6.disable=1` argument.",
55 | 'is_etcd_server': "Run etcd server on this node and connect other nodes to it.",
56 | 'is_k8s_schedulable': "Run kubelet on this node and register it as schedulable.",
57 | 'is_k8s_master': "Run kubelet on this node and add persistent kube-apiserver, kube-controller-manager, "
58 | "kube-scheduler pods to it.",
59 | }
60 | inline_models = [
61 | (models.Mountpoint, {
62 | 'column_descriptions': {
63 | 'what': 'Device to mount.',
64 | 'where': 'Mount path.',
65 | 'wanted_by': 'WantedBy systemd unit.',
66 | 'is_persistent': 'Use this partition to store critical data that should survive reboots.',
67 | }
68 | }),
69 | (models.Address, {
70 | 'column_descriptions': {
71 | 'interface': 'Network interface.',
72 | 'ip': 'IP address.',
73 | }
74 | }),
75 | ]
76 | form_rules = [
77 | rules.Field('cluster'),
78 | rules.Field('ip'),
79 | rules.Field('fqdn'),
80 | rules.FieldSet([
81 | 'maintenance_mode',
82 | 'debug_boot',
83 | 'coreos_autologin',
84 | 'linux_consoles',
85 | 'disable_ipv6',
86 | 'mountpoints',
87 | 'addresses',
88 | 'additional_kernel_cmdline',
89 | ], 'Boot'),
90 | rules.FieldSet([
91 | 'is_etcd_server',
92 | 'is_k8s_schedulable',
93 | 'is_k8s_master',
94 | ], 'Components'),
95 | ]
96 |
97 | # without this, Node is saved to database before on_model_change() gets called
98 | # and this happens only when there is inline_models
99 | def create_model(self, *args, **kwargs):
100 | with self.session.no_autoflush:
101 | return super().create_model(*args, **kwargs)
102 |
103 | def _issue_creds(self, model):
104 | with self.session.no_autoflush:
105 | ca_creds = model.cluster.ca_credentials
106 | creds = models.CredentialsData()
107 |
108 | creds.cert, creds.key = pki.issue_certificate('system:node:' + model.fqdn,
109 | ca_cert=ca_creds.cert,
110 | ca_key=ca_creds.key,
111 | organizations=['system:nodes'],
112 | san_dns=model.certificate_alternative_dns_names,
113 | san_ips=model.certificate_alternative_ips,
114 | certify_days=10000,
115 | is_web_server=True,
116 | is_web_client=True)
117 | self.session.add(creds)
118 | model.credentials = creds
119 |
120 | def on_model_change(self, form, model, is_created):
121 | if is_created:
122 | self._issue_creds(model)
123 | else:
124 | model.target_config_version += 1
125 |
126 | def on_model_delete(self, model):
127 | model.mountpoints.delete()
128 | model.addresses.delete()
129 |
130 | def after_model_delete(self, model):
131 | models.CredentialsData.query.filter_by(id=model.credentials_id).delete()
132 |
133 | @expose('/reissue-credentials', methods=['POST'])
134 | def reissue_creds_view(self):
135 | model = self.get_one(request.args.get('id'))
136 | model.target_config_version += 1
137 | self._issue_creds(model)
138 | self.session.add(model)
139 | self.session.commit()
140 | return_url = get_redirect_target() or self.get_url('.index_view')
141 | flash('The credentials successfully reissued', 'success')
142 | return redirect(return_url)
143 |
144 | @expose('/target-ignition.json')
145 | def target_ignition_config_view(self):
146 | node = self.get_one(request.args.get('id'))
147 | response = config_renderer.ignition.render(node, indent=True)
148 | return Response(response, mimetype='application/json')
149 |
150 | @expose('/target.ipxe')
151 | def target_ipxe_config_view(self):
152 | node = self.get_one(request.args.get('id'))
153 | response = config_renderer.ipxe.render(node, request.url_root)
154 | return Response(response, mimetype='text/plain')
155 |
--------------------------------------------------------------------------------
/src/seedbox/admin/provision.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from flask import request, Response
4 | from flask_admin import expose
5 | from flask_admin.model.template import macro
6 |
7 | from .base import ModelView
8 | from seedbox import ignition_parser
9 |
10 |
11 | class ProvisionView(ModelView):
12 | column_list = ['applied_at', 'config_version', 'data']
13 | column_details_list = ['applied_at', 'config_version', 'data']
14 | column_default_sort = ('applied_at', True)
15 | list_template = 'admin/provision_list.html'
16 | details_template = 'admin/provision_details.html'
17 | column_formatters = {
18 | 'data': macro('render_data'),
19 | }
20 |
21 | def get_query(self):
22 | query = super().get_query()
23 | node_id = request.args.get('node_id')
24 | if node_id:
25 | query = query.filter_by(node_id=node_id)
26 | return query
27 |
28 | @expose('/ipxe')
29 | def raw_ipxe_view(self):
30 | provision = self.get_one(request.args.get('id'))
31 | return Response(provision.ipxe_config, mimetype='text/plain')
32 |
33 | @expose('/ignition.json')
34 | def raw_ignition_view(self):
35 | provision = self.get_one(request.args.get('id'))
36 | data = json.loads(provision.ignition_config)
37 | data = json.dumps(data, indent=2)
38 | return Response(data, mimetype='application/json')
39 |
40 | @expose('/ignition.tar.gz')
41 | def ignition_filesystem_view(self):
42 | provision = self.get_one(request.args.get('id'))
43 | tgz_data = ignition_parser.render_ignition_tgz(json.loads(provision.ignition_config))
44 | return Response(tgz_data, mimetype='application/tar+gzip')
45 |
46 | def is_visible(self):
47 | return False
48 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/cluster_details.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/details.html' %}
2 | {% from 'admin/cluster_macros.html' import render_ca_credentials %}
3 | {% from 'admin/cluster_macros.html' import render_info %}
4 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/cluster_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/list.html' %}
2 | {% from 'admin/cluster_macros.html' import render_ca_credentials,render_info with context %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/cluster_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_ca_credentials(model, column) %}
2 | [cert]
3 | [text]
4 | [key]
5 |
6 |
11 |
12 | {% set ca_credentials_error = model.ca_credentials_error %}
13 | {% if ca_credentials_error %}
14 |
15 | {% endif %}
16 | {% endmacro %}
17 |
18 | {% macro render_info(model, column) %}
19 |
24 |
25 |
26 | {% if not model.k8s_kube_proxy_user %}
27 | no system:kube-proxy user
28 | {% endif %}
29 |
30 |
31 |
32 |
33 |
40 |
41 |
42 |
43 | -
44 | CoreOS: {{ model.coreos_channel }} {{ model.coreos_version }}
45 | {% if admin_view.latest_component_versions and admin_view.latest_component_versions.coreos[model.coreos_channel] and model.coreos_version != admin_view.latest_component_versions.coreos[model.coreos_channel] %}
46 |
47 | {% endif %}
48 |
49 | -
50 | etcd: {{ model.etcd_image_tag }}
51 | {% if admin_view.latest_component_versions and admin_view.latest_component_versions.etcd and model.etcd_image_tag != admin_view.latest_component_versions.etcd %}
52 |
53 | {% endif %}
54 |
55 | -
56 | hyperkube: {{ model.k8s_hyperkube_tag }}
57 | {% if admin_view.latest_component_versions and admin_view.latest_component_versions.hyperkube and model.k8s_hyperkube_tag != admin_view.latest_component_versions.hyperkube %}
58 |
59 | {% endif %}
60 |
61 | -
62 | etcd servers:
63 |
64 | {% set etcd_servers_count = model.nodes.filter_by(is_etcd_server=True).count() %}
65 | {% if etcd_servers_count and etcd_servers_count % 2 == 0 %}
66 | - Even etcd servers number!
67 | {% endif %}
68 | {% for node in model.nodes.filter_by(is_etcd_server=True) %}
69 | - {{ node.fqdn }}
70 | {% endfor %}
71 |
72 |
73 | -
74 | Kubernetes master nodes:
75 |
76 | {% for node in model.nodes.filter_by(is_k8s_master=True) %}
77 | - {{ node.fqdn }}
78 | {% endfor %}
79 |
80 |
81 | -
82 | Kubernetes schedulable nodes:
83 |
84 | {% for node in model.nodes.filter_by(is_k8s_schedulable=True) %}
85 | - {{ node.fqdn }}
86 | {% endfor %}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
101 |
102 |
103 |
104 | -
105 | Download kubeconfig and activate it:
106 | $ export KUBECONFIG=/path/to/kubeconfig
107 |
108 | -
109 | Init helm:
110 | $ helm init
111 |
112 | -
113 | If you have RBAC enabled, install helm tiller workaround.
114 |
115 | -
116 | Install any addons:
117 | $ helm install -n RELEASE_NAME CHART_URL
.
118 | Where RELEASE_NAME is any name you like, and CHART_URL is one of the following:
119 |
120 | {% for addon_name, addon_versions in admin_view.k8s_addons_dict.items() %}
121 | -
122 | {{ addon_name }}
123 | {% for addon_version in addon_versions.keys() %}
124 | {{ addon_version }}
125 | {% endfor %}
126 |
127 | {% endfor %}
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | {% endmacro %}
136 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/credentials_details.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/details.html' %}
2 | {% from 'admin/credentials_macros.html' import render_cert, render_key %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/credentials_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/list.html' %}
2 | {% from 'admin/credentials_macros.html' import render_cert, render_key %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/credentials_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_cert(model, column) %}
2 | raw
3 | text
4 | {% endmacro %}
5 |
6 | {% macro render_key(model, column) %}
7 | raw
8 | {% endmacro %}
9 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/master.html' %}
2 |
3 | {% block body %}
4 | Usage manual
5 |
6 |
7 | - Define a cluster in Cluster tab.
8 | - Add some nodes to cluster in Node tab. You will need at least one node with etcd server (three
9 | for fault tolerance), one k8s master (two for HA) and one k8s schedulable node. For testing purposes
10 | this can be a single node.
11 | - Add a user with
system:masters
group and download generated kubeconfig.
12 | - Configure your cluster servers for iPXE boot with this script:
13 |
14 | #!ipxe
15 | chain http://seedbox_host:seedbox_port/ipxe
16 |
17 |
18 | - Startup the servers.
19 | - After some time required to bootstrap full k8s cluster, you will be able to access it using
20 |
kubectl
and downloaded kubeconfig.
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/node_details.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/details.html' %}
2 | {% from 'admin/node_macros.html' import render_credentials, render_config %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/node_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/list.html' %}
2 | {% from 'admin/node_macros.html' import render_credentials, render_config %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/node_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_credentials(model, column) %}
2 | [cert]
3 | [text]
4 | [key]
5 |
6 |
11 |
12 | {% set credentials_error=model.credentials_error %}
13 | {% if credentials_error %}
14 |
15 | {% else %}
16 | {% set credentials_warning=model.credentials_warning %}
17 | {% if credentials_warning %}
18 |
19 | {% endif %}
20 | {% endif %}
21 | {% endmacro %}
22 |
23 | {% macro render_config(model, column) %}
24 | {% if model.is_config_match %}
25 |
26 | {% else %}
27 |
28 | {% endif %}
29 |
30 | [target ignition]
31 | [target ipxe]
32 |
33 | {% if model.active_config %}
34 | [active]
35 | {% endif %}
36 |
37 | [history]
38 |
39 | {% set disks_error=model.disks_error %}
40 | {% if disks_error %}
41 |
42 | {% else %}
43 | {% set disks_warning=model.disks_warning %}
44 | {% if disks_warning %}
45 |
46 | {% endif %}
47 | {% endif %}
48 | {% endmacro %}
49 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/provision_details.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/details.html' %}
2 | {% from 'admin/provision_macros.html' import render_data %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/provision_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/list.html' %}
2 | {% from 'admin/provision_macros.html' import render_data %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/provision_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_data(model, column) %}
2 | {% if model.ignition_config %}
3 | [raw ignition]
4 | [ignition fs]
5 | {% endif %}
6 |
7 | {% if model.ipxe_config %}
8 | [raw ipxe]
9 | {% endif %}
10 | {% endmacro %}
11 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/user_details.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/details.html' %}
2 | {% from 'admin/user_macros.html' import render_credentials, render_kubeconfig %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/user_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/list.html' %}
2 | {% from 'admin/user_macros.html' import render_credentials, render_kubeconfig %}
3 |
--------------------------------------------------------------------------------
/src/seedbox/admin/templates/admin/user_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_credentials(model, column) %}
2 | [cert]
3 | [text]
4 | [key]
5 |
6 |
11 |
12 | {% set credentials_error=model.credentials_error %}
13 | {% if credentials_error %}
14 |
15 | {% endif %}
16 | {% endmacro %}
17 |
18 | {% macro render_kubeconfig(model, column) %}
19 | raw
20 | {% endmacro %}
21 |
--------------------------------------------------------------------------------
/src/seedbox/admin/user.py:
--------------------------------------------------------------------------------
1 | from flask import request, Response, flash, redirect, abort
2 | from flask_admin import expose
3 | from flask_admin.form import rules
4 | from flask_admin.actions import action
5 | from flask_admin.helpers import get_redirect_target
6 | from flask_admin.model.template import macro
7 |
8 | from seedbox import pki, models, config_renderer, exceptions
9 | from .base import ModelView
10 |
11 |
12 | class UserView(ModelView):
13 | column_list = ['cluster', 'name', 'credentials', 'kubeconfig']
14 | list_template = 'admin/user_list.html'
15 | details_template = 'admin/user_details.html'
16 | form_excluded_columns = [
17 | 'credentials',
18 | ]
19 | column_formatters = {
20 | 'credentials': macro('render_credentials'),
21 | 'kubeconfig': macro('render_kubeconfig'),
22 | }
23 | column_labels = {
24 | 'ssh_key': 'SSH key',
25 | 'k8s_groups': 'Kubernetes groups',
26 | }
27 | column_descriptions = {
28 | 'name': 'Will be used as CommonName in TLS certificate.',
29 | 'k8s_groups': 'Will be added as Organization(s) in TLS certificate. (Separate by comma.)',
30 | 'ssh_key': 'This key will be authorized on all nodes of the cluster (as `core` user).',
31 | }
32 | form_rules = [
33 | rules.Field('cluster'),
34 | rules.Field('name'),
35 | rules.Field('k8s_groups'),
36 | rules.Field('ssh_key'),
37 | ]
38 |
39 | def _issue_creds(self, model):
40 | with self.session.no_autoflush:
41 | ca_creds = model.cluster.ca_credentials
42 | creds = models.CredentialsData()
43 | if model.k8s_groups:
44 | orgs = model.k8s_groups.split(',')
45 | else:
46 | orgs = []
47 | creds.cert, creds.key = pki.issue_certificate(model.name,
48 | ca_cert=ca_creds.cert,
49 | ca_key=ca_creds.key,
50 | organizations=orgs,
51 | certify_days=365,
52 | is_web_client=True)
53 | self.session.add(creds)
54 | model.credentials = creds
55 |
56 | def on_model_change(self, form, model, is_created):
57 | if is_created:
58 | self._issue_creds(model)
59 |
60 | def after_model_delete(self, model):
61 | models.CredentialsData.query.filter_by(id=model.credentials_id).delete()
62 |
63 | @expose('/reissue-credentials', methods=['POST'])
64 | def reissue_creds_view(self):
65 | model = self.get_one(request.args.get('id'))
66 | self._issue_creds(model)
67 | self.session.add(model)
68 | self.session.commit()
69 | return_url = get_redirect_target() or self.get_url('.index_view')
70 | flash('The credentials successfully reissued', 'success')
71 | return redirect(return_url)
72 |
73 | @expose('/kubeconfig')
74 | def kubeconfig_view(self):
75 | user = self.get_one(request.args.get('id'))
76 | return Response(render_kubeconfig([user]), mimetype='text/x-yaml')
77 |
78 | @action('kubeconfig', 'Get kubeconfig')
79 | def kubeconfig_action(self, ids):
80 | users = models.User.query.filter(models.User.id.in_(ids))
81 | return Response(render_kubeconfig(list(users)), mimetype='text/x-yaml')
82 |
83 |
84 | def render_kubeconfig(users):
85 | try:
86 | default_user = None
87 | if len(users) == 1:
88 | default_user = users[0]
89 |
90 | return config_renderer.kubeconfig.render(users, default_user=default_user)
91 | except exceptions.K8sNoClusterApiserver as e:
92 | return abort(404, str(e))
93 |
--------------------------------------------------------------------------------
/src/seedbox/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | from flask import Flask, Response, request, abort, redirect
5 | from flask_migrate import Migrate
6 | from werkzeug.contrib.fixers import ProxyFix
7 |
8 | from seedbox import config, models, config_renderer
9 | from seedbox.admin import admin
10 |
11 |
12 | log = logging.getLogger(__name__)
13 |
14 |
15 | app = Flask(__name__, template_folder='admin/templates')
16 | if config.reverse_proxy_count:
17 | app.wsgi_app = ProxyFix(app.wsgi_app, num_proxies=config.reverse_proxy_count)
18 | app.secret_key = config.secret_key
19 | app.config['SQLALCHEMY_DATABASE_URI'] = config.database_uri
20 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
21 | models.db.app = app
22 | models.db.init_app(app)
23 | migrate = Migrate(app, models.db, directory=os.path.join(config.app_root, 'seedbox', 'migrations'))
24 | admin.init_app(app)
25 |
26 |
27 | def get_node(node_ip):
28 | node = models.Node.query.filter_by(ip=node_ip).first()
29 | if node is None:
30 | log.error('Node %s is unknown', node_ip)
31 | return abort(403)
32 | return node
33 |
34 |
35 | @app.before_request
36 | def check_admin_secure():
37 | if request.path.startswith(config.admin_base_url + '/'):
38 | response = ensure_secure_request() or ensure_admin_authenticated()
39 | if response is not None:
40 | return response
41 |
42 |
43 | def route(rule, request_name, secure=True, **route_kwargs):
44 | def decorator(func):
45 | def wrapped(*args, **kwargs):
46 | node_ip = request.remote_addr
47 | log.info('%s request from %s', request_name, node_ip)
48 | if secure:
49 | response = ensure_secure_request()
50 | if response is not None:
51 | return response
52 | node = get_node(node_ip)
53 | return func(node, *args, **kwargs)
54 |
55 | wrapped.__name__ = func.__name__
56 | return app.route(rule, **route_kwargs)(wrapped)
57 | return decorator
58 |
59 |
60 | def ensure_secure_request():
61 | is_request_secure = request.environ['wsgi.url_scheme'] == 'https'
62 | if not is_request_secure and not config.allow_insecure_transport:
63 | if request.method in ('POST', 'PUT', 'PATCH'):
64 | # request body already sent in insecure manner
65 | # return error in this case to notify cluster admin
66 | return abort(400)
67 | else:
68 | return redirect(request.url.replace('http://', 'https://', 1))
69 |
70 |
71 | def ensure_admin_authenticated():
72 | auth = request.authorization
73 | if not auth or auth.username != config.admin_username or auth.password != config.admin_password:
74 | return Response('401 Unauthorized', 401, {
75 | 'WWW-Authenticate': 'Basic realm="Login Required"'
76 | })
77 |
78 |
79 | @route('/ipxe', 'iPXE boot', secure=False)
80 | def ipxe_boot(node):
81 | response = config_renderer.ipxe.render(node, request.url_root)
82 | return Response(response, mimetype='text/plain')
83 |
84 |
85 | @route('/ignition', 'Ignition config')
86 | def ignition(node):
87 | response = config_renderer.ignition.render(node, 'indent' in request.args)
88 | return Response(response, mimetype='application/json')
89 |
90 |
91 | @route('/credentials/.pem', 'Credentials download')
92 | def credentials(node, cred_type):
93 | if cred_type == 'ca':
94 | return Response(node.cluster.ca_credentials.cert, mimetype='text/plain')
95 |
96 | if cred_type == 'node':
97 | return Response(node.credentials.cert, mimetype='text/plain')
98 |
99 | if cred_type == 'node-key':
100 | return Response(node.credentials.key, mimetype='text/plain')
101 |
102 | return abort(404)
103 |
104 |
105 | @route('/report', 'Provision report', methods=['POST'])
106 | def report(node):
107 | if not node.maintenance_mode:
108 | try:
109 | node.active_config_version = int(request.args.get('version'))
110 | except (ValueError, TypeError):
111 | return abort(400)
112 |
113 | if request.content_type != 'application/json':
114 | return abort(400)
115 |
116 | provision = models.Provision()
117 | provision.node = node
118 | provision.config_version = node.active_config_version
119 | provision.ignition_config = request.data
120 | if node.target_config_version == node.active_config_version:
121 | provision.ipxe_config = config_renderer.ipxe.render(node, request.url_root)
122 | models.db.session.add(provision)
123 |
124 | models.db.session.add(node)
125 |
126 | node.disks.update({
127 | models.Disk.wipe_next_boot: False
128 | })
129 |
130 | if node.cluster.are_etcd_nodes_configured:
131 | node.cluster.assert_etcd_cluster_exists = True
132 | models.db.session.add(node.cluster)
133 |
134 | models.db.session.commit()
135 | return Response('ok', mimetype='application/json')
136 |
137 |
138 | @app.route('/addons//-.tar.gz')
139 | def k8s_addons_helm_chart(cluster_name, addon_name, addon_version):
140 | cluster = models.Cluster.query.filter_by(name=cluster_name).first()
141 | if cluster is None:
142 | return abort(404)
143 |
144 | try:
145 | addon = config_renderer.charts.addons[addon_name][addon_version]
146 | except KeyError:
147 | return abort(404)
148 |
149 | return Response(config_renderer.charts.render_addon_tgz(cluster, addon),
150 | mimetype='application/tar+gzip')
151 |
--------------------------------------------------------------------------------
/src/seedbox/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | app_root = os.path.dirname(os.path.dirname(__file__))
4 | dev_root = os.path.dirname(os.path.dirname(app_root))
5 |
6 | secret_key = os.environ.get('SECRET_KEY', '-')
7 | allow_insecure_transport = os.environ.get('ALLOW_INSECURE_TRANSPORT', '1').lower() in ('1', 'true', 'yes')
8 | admin_username = os.environ.get('ADMIN_USERNAME', 'admin')
9 | admin_password = os.environ.get('ADMIN_PASSWORD', 'admin')
10 | database_uri = os.environ.get('DATABASE_URI', 'sqlite:///' + os.path.join(dev_root, 'test.db'))
11 | reverse_proxy_count = int(os.environ.get('REVERSE_PROXY_COUNT', 0))
12 |
13 | update_check_interval_sec = 24 * 60 * 60 # 24 hours
14 | update_state_file = os.environ.get('UPDATE_STATE_FILE', '/tmp/seedbox.json')
15 |
16 | admin_base_url = '/admin'
17 |
18 | default_coreos_channel = 'stable'
19 | default_coreos_version = '1353.7.0' # 'current' is also applicable
20 | default_coreos_images_base_url = 'http://{channel}.release.core-os.net/amd64-usr/{version}/'
21 | etcd_image = 'quay.io/coreos/etcd'
22 | default_etcd_image_tag = 'v3.1.7'
23 | k8s_hyperkube_image = 'quay.io/coreos/hyperkube'
24 | default_k8s_hyperkube_tag = 'v1.6.2_coreos.0'
25 | default_k8s_pod_network = '10.2.0.0/16'
26 | default_k8s_service_network = '10.3.0.0/24'
27 | default_k8s_admission_control = 'NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,' \
28 | 'DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds'
29 | default_linux_consoles = 'tty0,ttyS0'
30 |
31 | etcd_client_port = 2379
32 | etcd_peer_port = 2380
33 | k8s_apiserver_secure_port = 6443
34 | k8s_apiserver_insecure_port = 8080
35 | k8s_cluster_domain = 'cluster.local'
36 |
37 | cluster_credentials_path = '/etc/ssl/cluster'
38 | ca_cert_filename = 'ca.pem'
39 | node_cert_filename = 'node.pem'
40 | node_key_filename = 'node-key.pem'
41 | ca_cert_path = cluster_credentials_path + '/' + ca_cert_filename
42 | node_cert_path = cluster_credentials_path + '/' + node_cert_filename
43 | node_key_path = cluster_credentials_path + '/' + node_key_filename
44 |
45 | k8s_config_path = '/etc/kubernetes' # don't change. hardcoded in kubelet-wrapper
46 | k8s_secrets_path = k8s_config_path + '/secrets'
47 | k8s_service_account_public_key_path = k8s_secrets_path + '/service-account.pub'
48 | k8s_service_account_private_key_path = k8s_secrets_path + '/service-account.priv'
49 | k8s_manifests_path = k8s_config_path + '/manifests'
50 | k8s_kubeconfig_path = k8s_config_path + '/kubeconfig.yaml'
51 | k8s_kube_proxy_config_path = k8s_config_path + '/kube-proxy-config.yaml'
52 | k8s_cni_conf_path = k8s_config_path + '/cni/net.d'
53 | k8s_kube_proxy_user_name = 'system:kube-proxy'
54 |
55 | node_ca_certificates_path = '/usr/share/ca-certificates'
56 |
57 | aci_proxy_ca_cert_path = '/etc/ssl/certs/aci-proxy-ca.pem'
58 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/__init__.py:
--------------------------------------------------------------------------------
1 | from . import ipxe
2 | from . import charts
3 | from . import ignition
4 | from . import kubeconfig
5 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/charts.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 | import re
4 | from urllib.parse import urljoin
5 |
6 | import requests
7 | from jinja2 import Environment
8 |
9 | from seedbox import config, utils
10 |
11 | NOT_SPECIFIED = object()
12 | jinja_var_env = Environment(autoescape=False)
13 | jinja_env = Environment(keep_trailing_newline=True, autoescape=False)
14 |
15 |
16 | class Addon:
17 | base_url = 'https://github.com/kubernetes/kubernetes/raw/{branch}/cluster/addons/'
18 | encoding = 'utf-8'
19 |
20 | def __init__(self, name, version, manifest_files, vars_map=None, is_salt_template=False, base_url=None, notes=None):
21 | if vars_map is None:
22 | vars_map = {}
23 |
24 | if base_url is None:
25 | base_url = urljoin(self.base_url, name + '/')
26 | else:
27 | base_url = urljoin(self.base_url, base_url)
28 |
29 | if version == 'master':
30 | branch = 'master'
31 | else:
32 | branch = 'release-' + version
33 | base_url = base_url.format(branch=branch)
34 |
35 | self.name = name
36 | self.version = version
37 |
38 | self.manifest_files = []
39 | for fname in manifest_files:
40 | fname = urljoin(base_url, fname)
41 | self.manifest_files.append(fname)
42 |
43 | self.vars_map = vars_map
44 | self.is_salt_template = is_salt_template
45 | self.notes = notes
46 |
47 | def render_files(self, cluster):
48 | yield 'Chart.yaml', self.render_chart_yaml()
49 | yield 'values.yaml', self.render_values_yaml(cluster)
50 |
51 | for url in self.manifest_files:
52 | filename, content = self.render_manifest_file(cluster, url)
53 | yield os.path.join('templates', filename), content
54 |
55 | if self.notes:
56 | yield os.path.join('templates', 'NOTES.txt'), self.notes.encode(self.encoding)
57 |
58 | def render_chart_yaml(self):
59 | return 'name: {}\nversion: {}\n'.format(self.name, self.version).encode(self.encoding)
60 |
61 | def render_values_yaml(self, cluster):
62 | return ''.join('{}: {}\n'.format(var_name, jinja_var_env.from_string(var_tpl).render({
63 | 'config': config,
64 | 'cluster': cluster,
65 | })) for var_name, var_tpl in self.vars_map.items()).encode(self.encoding)
66 |
67 | def render_manifest_file(self, cluster, url):
68 | pillar = SaltPillarEmulator(cluster)
69 |
70 | resp = requests.get(url)
71 | resp.raise_for_status()
72 |
73 | content = resp.content
74 | if self.is_salt_template:
75 | t = jinja_env.from_string(content.decode(self.encoding))
76 | content = t.render({
77 | 'pillar': pillar,
78 | }).encode(self.encoding)
79 | else:
80 | for var_name in self.vars_map.keys():
81 | var_name = var_name.encode(self.encoding)
82 | content = content.replace(b'$' + var_name, b'{{ .Values.%s }}' % var_name)
83 |
84 | filename = os.path.basename(url)
85 | m = re.match(r'(.*\.yaml).*', filename)
86 | if m:
87 | filename = m.group(1)
88 |
89 | return filename, content
90 |
91 |
92 | class SaltPillarEmulator:
93 | def __init__(self, cluster):
94 | self.cluster = cluster
95 |
96 | def get(self, var_name, default=NOT_SPECIFIED):
97 | try:
98 | return getattr(self, '_' + var_name)
99 | except AttributeError:
100 | if default is NOT_SPECIFIED:
101 | raise
102 | else:
103 | return default
104 |
105 | @property
106 | def _num_nodes(self):
107 | return self.cluster.nodes.count()
108 |
109 | dashboard_notes = '''1. Start kube proxy:
110 | $ kubectl proxy
111 | 2. Open dashboard in a browser: http://localhost:8001/ui/
112 | '''
113 |
114 | addons = {
115 | 'dns': {
116 | '1.5': Addon('dns', '1.5', [
117 | 'skydns-rc.yaml.sed',
118 | 'skydns-svc.yaml.sed',
119 | ], {
120 | 'DNS_DOMAIN': '{{ config.k8s_cluster_domain }}',
121 | 'DNS_SERVER_IP': '{{ cluster.k8s_dns_service_ip }}',
122 | }),
123 | '1.6': Addon('dns', '1.6', [
124 | 'kubedns-cm.yaml',
125 | 'kubedns-sa.yaml',
126 | 'kubedns-controller.yaml.sed',
127 | 'kubedns-svc.yaml.sed',
128 | ], {
129 | 'DNS_DOMAIN': '{{ config.k8s_cluster_domain }}',
130 | 'DNS_SERVER_IP': '{{ cluster.k8s_dns_service_ip }}',
131 | }),
132 | },
133 | 'dns-horizontal-autoscaler': {
134 | '1.5': Addon('dns-horizontal-autoscaler', '1.5', ['dns-horizontal-autoscaler.yaml']),
135 | '1.6': Addon('dns-horizontal-autoscaler', '1.6', ['dns-horizontal-autoscaler.yaml']),
136 | 'master': Addon('dns-horizontal-autoscaler', 'master', [
137 | 'dns-horizontal-autoscaler.yaml',
138 | 'dns-horizontal-autoscaler-rbac.yaml',
139 | ]),
140 | },
141 | 'dashboard': {
142 | '1.5': Addon('dashboard', '1.5', [
143 | 'dashboard-controller.yaml',
144 | 'dashboard-service.yaml',
145 | ], notes=dashboard_notes),
146 | '1.6': Addon('dashboard', '1.6', ['kubernetes-dashboard.yaml'],
147 | notes=dashboard_notes,
148 | base_url='https://github.com/kubernetes/dashboard/raw/master/src/deploy/'),
149 | },
150 | 'heapster': {
151 | '1.5': Addon('heapster', '1.5', [
152 | 'heapster-controller.yaml',
153 | 'heapster-service.yaml',
154 | ], is_salt_template=True, base_url='cluster-monitoring/standalone/'),
155 | '1.6': Addon('heapster', '1.6', [
156 | 'heapster-controller.yaml',
157 | 'heapster-service.yaml',
158 | ], is_salt_template=True, base_url='cluster-monitoring/standalone/', notes='RBAC instructions: '
159 | 'https://gist.github.com/nailgun/5a4413c8e2fd0bba8e8aa443e8ba9cee'),
160 | },
161 | 'fluentd-elasticsearch': {
162 | '1.6': Addon('fluentd-elasticsearch', '1.6', [
163 | 'es-controller.yaml',
164 | 'es-service.yaml',
165 | 'fluentd-es-ds.yaml',
166 | 'kibana-controller.yaml',
167 | 'kibana-service.yaml',
168 | ], notes='Documentation: '
169 | 'https://kubernetes.io/docs/tasks/debug-application-cluster/logging-elasticsearch-kibana/'),
170 | },
171 | }
172 |
173 |
174 | def render_addon_tgz(cluster, addon):
175 | tgz_fp = io.BytesIO()
176 | with utils.TarFile.open(fileobj=tgz_fp, mode='w:gz') as tgz:
177 | for path, content in addon.render_files(cluster):
178 | tgz.adddata(os.path.join(addon.name, path), content)
179 | return tgz_fp.getvalue()
180 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/__init__.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import json
3 |
4 | from flask import request
5 |
6 | from seedbox import models
7 |
8 |
9 | def render(node, indent=False):
10 | return IgnitionConfig(node).render(indent)
11 |
12 |
13 | class IgnitionConfig(object):
14 | def __init__(self, node):
15 | self.node = node
16 | self.cluster = node.cluster
17 |
18 | def render(self, indent=False):
19 | content = self.get_content()
20 | if indent:
21 | return json.dumps(content, indent=2)
22 | else:
23 | return json.dumps(content, separators=(',', ':'))
24 |
25 | def get_content(self):
26 | packages = [P(self.node, request.url_root) for P in self.get_package_classes()]
27 | files = list(itertools.chain.from_iterable(p.get_files() for p in packages))
28 | units = list(itertools.chain.from_iterable(p.get_units() for p in packages))
29 | networkd_units = list(itertools.chain.from_iterable(p.get_networkd_units() for p in packages))
30 | ssh_keys = self.get_ssh_keys()
31 |
32 | return {
33 | 'ignition': {
34 | 'version': '2.0.0',
35 | 'config': {},
36 | },
37 | 'storage': self.get_storage_config(files),
38 | 'networkd': {
39 | 'units': networkd_units
40 | },
41 | 'passwd': {
42 | 'users': [{
43 | 'name': 'root',
44 | 'sshAuthorizedKeys': ssh_keys,
45 | }, {
46 | 'name': 'core',
47 | 'sshAuthorizedKeys': ssh_keys,
48 | }],
49 | },
50 | 'systemd': {
51 | 'units': units
52 | },
53 | }
54 |
55 | def get_package_classes(self):
56 | from .system import SystemPackage
57 |
58 | if self.node.maintenance_mode:
59 | return [SystemPackage]
60 |
61 | from .credentials import CredentialsPackage
62 | from .flannel import FlannelPackage
63 |
64 | packages = [
65 | SystemPackage,
66 | CredentialsPackage,
67 | FlannelPackage,
68 | ]
69 |
70 | if self.cluster.install_dnsmasq:
71 | from .dnsmasq import DnsmasqPackage
72 | packages += [
73 | DnsmasqPackage,
74 | ]
75 |
76 | if self.node.is_etcd_server:
77 | from .etcd_server import EtcdServerPackage
78 | packages += [
79 | EtcdServerPackage,
80 | ]
81 |
82 | if self.node.is_k8s_schedulable or self.node.is_k8s_master:
83 | from .kubeconfig import KubeconfigPackage
84 | from .kubelet import KubeletPackage
85 | from .kube_proxy import KubeProxyPackage
86 | packages += [
87 | KubeconfigPackage,
88 | KubeletPackage,
89 | KubeProxyPackage,
90 | ]
91 |
92 | if self.node.is_k8s_master:
93 | from .k8s_master_manifests import K8sMasterManifestsPackage
94 | packages += [
95 | K8sMasterManifestsPackage,
96 | ]
97 |
98 | return packages
99 |
100 | def get_storage_config(self, files):
101 | disks = []
102 | filesystems = []
103 |
104 | config = {
105 | 'disks': disks,
106 | 'filesystems': filesystems,
107 | 'files': files,
108 | }
109 |
110 | if self.node.maintenance_mode:
111 | return config
112 |
113 | root_fs = False
114 |
115 | for disk in self.node.disks.filter_by(wipe_next_boot=True):
116 | partitions = []
117 |
118 | disks += [{
119 | 'device': disk.device,
120 | 'wipeTable': True,
121 | 'partitions': partitions,
122 | }]
123 |
124 | for partition in disk.partitions.all():
125 | if partition.size_mibs:
126 | size_sectors = partition.size_mibs * 1024 * 1024 // disk.sector_size_bytes
127 | else:
128 | size_sectors = 0
129 |
130 | partitions += [{
131 | 'number': partition.number,
132 | 'start': 0,
133 | 'size': size_sectors,
134 | 'label': partition.label,
135 | }]
136 |
137 | filesystems += [partition2ignitionfs(partition)]
138 |
139 | if partition.is_root:
140 | root_fs = True
141 |
142 | if not root_fs:
143 | filesystems += [partition2ignitionfs(self.node.root_partition)]
144 |
145 | return config
146 |
147 | def get_ssh_keys(self):
148 | return [user.ssh_key for user in self.cluster.users.filter(models.User.ssh_key != '')]
149 |
150 |
151 | def partition2ignitionfs(partition):
152 | fs = {
153 | 'mount': {
154 | 'device': partition.device,
155 | 'format': partition.format,
156 | 'create': {
157 | 'force': True,
158 | 'options': ['-L{}'.format(partition.label)],
159 | },
160 | },
161 | }
162 |
163 | if partition.is_root:
164 | fs['name'] = 'root'
165 |
166 | return fs
167 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/base.py:
--------------------------------------------------------------------------------
1 | import os
2 | import base64
3 | import inspect
4 | import urllib.parse
5 |
6 | from flask.helpers import locked_cached_property
7 | from jinja2 import Environment, ChoiceLoader, FileSystemLoader
8 |
9 | from seedbox import config
10 |
11 |
12 | class BaseIgnitionPackage(object):
13 | name = None
14 | template_context = {}
15 |
16 | def __init__(self, node, url_root):
17 | self.node = node
18 | self.cluster = node.cluster
19 | self.url_root = url_root
20 |
21 | def get_files(self):
22 | return ()
23 |
24 | def get_units(self):
25 | return ()
26 |
27 | def get_networkd_units(self):
28 | return ()
29 |
30 | def enable_unit(self, name):
31 | return {
32 | 'name': name,
33 | 'enable': True,
34 | }
35 |
36 | def get_unit(self, name, enable=False, template_name=None, additional_context=None):
37 | if template_name is None:
38 | template_name = name
39 | unit = {
40 | 'name': name,
41 | 'contents': self.render_template(template_name, additional_context),
42 | }
43 | if enable:
44 | unit['enable'] = True
45 | return unit
46 |
47 | def get_unit_dropins(self, unitname, dropins, enableunit=False):
48 | dropin = {
49 | 'name': unitname,
50 | 'dropins': [{
51 | 'name': dropin,
52 | 'contents': self.render_template('{}.d/{}'.format(unitname, dropin)),
53 | } for dropin in dropins],
54 | }
55 | if enableunit:
56 | dropin['enable'] = True
57 | return dropin
58 |
59 | def get_full_template_context(self, additional_context=None):
60 | context = {
61 | 'config': config,
62 | 'node': self.node,
63 | 'cluster': self.cluster,
64 | 'url_root': self.url_root,
65 | }
66 |
67 | context.update(self.get_template_context())
68 |
69 | if additional_context:
70 | context.update(additional_context)
71 |
72 | return context
73 |
74 | def get_template_context(self):
75 | return self.template_context
76 |
77 | def render_template(self, name, additional_context=None):
78 | return self.jinja_env.get_template(name).render(self.get_full_template_context(additional_context))
79 |
80 | @locked_cached_property
81 | def jinja_env(self):
82 | return Environment(loader=self.create_template_loader(),
83 | keep_trailing_newline=True,
84 | autoescape=False)
85 |
86 | def create_template_loader(self):
87 | template_roots = []
88 |
89 | cls = self.__class__
90 | while True:
91 | template_roots.append(os.path.dirname(inspect.getfile(cls)))
92 | cls = get_base_class(cls)
93 | if cls in (BaseIgnitionPackage, None):
94 | break
95 |
96 | return ChoiceLoader([FileSystemLoader(root) for root in template_roots])
97 |
98 | @staticmethod
99 | def to_data_url(data, mediatype='', b64=False):
100 | if b64:
101 | if not isinstance(data, bytes):
102 | data = data.encode('utf-8')
103 | return 'data:{};base64,{}'.format(mediatype, base64.b64encode(data).decode('ascii'))
104 | else:
105 | return 'data:{},{}'.format(mediatype, urllib.parse.quote(data))
106 |
107 |
108 | def get_base_class(cls):
109 | for base in cls.__bases__:
110 | if issubclass(base, BaseIgnitionPackage):
111 | return base
112 | return None
113 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/credentials.py:
--------------------------------------------------------------------------------
1 | from seedbox import config
2 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
3 |
4 |
5 | class CredentialsPackage(BaseIgnitionPackage):
6 | def get_files(self):
7 | return [
8 | {
9 | 'filesystem': 'root',
10 | 'path': config.ca_cert_path,
11 | 'mode': 0o444,
12 | 'contents': {
13 | 'source': self.url_root + 'credentials/ca.pem',
14 | },
15 | },
16 | {
17 | 'filesystem': 'root',
18 | 'path': config.node_cert_path,
19 | 'mode': 0o444,
20 | 'contents': {
21 | 'source': self.url_root + 'credentials/node.pem',
22 | },
23 | },
24 | {
25 | 'filesystem': 'root',
26 | 'path': config.node_key_path,
27 | 'mode': 0o444,
28 | 'contents': {
29 | 'source': self.url_root + 'credentials/node-key.pem',
30 | },
31 | },
32 | ]
33 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/dnsmasq/__init__.py:
--------------------------------------------------------------------------------
1 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
2 |
3 |
4 | class DnsmasqPackage(BaseIgnitionPackage):
5 | def get_files(self):
6 | return [{
7 | 'filesystem': 'root',
8 | 'path': '/etc/systemd/resolved.conf.d/30-dnsmasq.conf',
9 | 'mode': 0o644,
10 | 'contents': {
11 | 'source': self.to_data_url(self.render_template('resolved.conf')),
12 | },
13 | }]
14 |
15 | def get_units(self):
16 | return [
17 | self.get_unit('dnsmasq.service', enable=True)
18 | ]
19 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/dnsmasq/dnsmasq.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=dnsmasq DNS service
3 |
4 | [Service]
5 | Restart=always
6 | RestartSec=10
7 | OOMScoreAdjust=-998
8 | ExecStart=/usr/bin/rkt run \
9 | --insecure-options=image \
10 | --net=host \
11 | --volume resolv,kind=host,source=/etc/resolv.conf \
12 | --mount volume=resolv,target=/etc/resolv.conf \
13 | quay.io/coreos/dnsmasq \
14 | -- \
15 | --keep-in-foreground \
16 | --user=root \
17 | --log-facility=- \
18 | --no-hosts \
19 | --server=/cluster.local/{{ cluster.k8s_dns_service_ip }} \
20 | {% if cluster.dnsmasq_static_records -%}
21 | {% for n in cluster.nodes.all() -%}
22 | --host-record={{ n.fqdn }},{{ n.ip }} \
23 | {% endfor -%}
24 | {% if cluster.etcd_nodes_dns_name -%}
25 | {% for n in cluster.nodes.filter_by(is_etcd_server=True) -%}
26 | --host-record={{ cluster.etcd_nodes_dns_name }},{{ n.ip }} \
27 | {% endfor -%}
28 | {% endif -%}
29 | {% if cluster.k8s_apiservers_dns_name -%}
30 | {% for n in cluster.nodes.filter_by(is_k8s_master=True) -%}
31 | --host-record={{ cluster.k8s_apiservers_dns_name }},{{ n.ip }} \
32 | {% endfor -%}
33 | {% endif -%}
34 | {% endif -%}
35 | \
36 |
37 | [Install]
38 | WantedBy=multi-user.target
39 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/dnsmasq/resolved.conf:
--------------------------------------------------------------------------------
1 | [Resolve]
2 | DNS={{ node.ip }}
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/etcd_server/__init__.py:
--------------------------------------------------------------------------------
1 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
2 |
3 |
4 | class EtcdServerPackage(BaseIgnitionPackage):
5 | def get_units(self):
6 | dropins = [
7 | '40-etcd-cluster.conf',
8 | '40-ssl.conf',
9 | '40-oom.conf',
10 | '30-image.conf',
11 | ]
12 |
13 | if self.node.persistent_mountpoint:
14 | dropins += ['40-persistent.conf']
15 |
16 | return [
17 | self.get_unit_dropins('etcd-member.service', dropins=dropins, enableunit=True),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/etcd_server/etcd-member.service.d/30-image.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="ETCD_IMAGE_URL={{ config.etcd_image }}"
3 | Environment="ETCD_IMAGE_TAG={{ cluster.etcd_image_tag }}"
4 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/etcd_server/etcd-member.service.d/40-etcd-cluster.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="ETCD_NAME={{ node.fqdn }}"
3 | Environment="ETCD_ADVERTISE_CLIENT_URLS=https://{{ node.fqdn }}:{{ config.etcd_client_port }}"
4 | Environment="ETCD_INITIAL_ADVERTISE_PEER_URLS=https://{{ node.fqdn }}:{{ config.etcd_peer_port }}"
5 | Environment="ETCD_LISTEN_CLIENT_URLS=https://0.0.0.0:{{ config.etcd_client_port }}"
6 | Environment="ETCD_LISTEN_PEER_URLS=https://0.0.0.0:{{ config.etcd_peer_port }}"
7 | Environment="ETCD_INITIAL_CLUSTER={% for n in cluster.etcd_nodes %}{{ n.fqdn }}=https://{{ n.fqdn }}:{{ config.etcd_peer_port }}{% if not loop.last %},{% endif %}{% endfor %}"
8 | Environment="ETCD_STRICT_RECONFIG_CHECK=true"
9 | {% if cluster.assert_etcd_cluster_exists -%}
10 | Environment="ETCD_INITIAL_CLUSTER_STATE=existing"
11 | {% endif -%}
12 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/etcd_server/etcd-member.service.d/40-oom.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | OOMScoreAdjust=-1000
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/etcd_server/etcd-member.service.d/40-persistent.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="ETCD_DATA_DIR={{ node.persistent_mountpoint.where }}/etcd"
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/etcd_server/etcd-member.service.d/40-ssl.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="ETCD_SSL_DIR={{ config.cluster_credentials_path }}"
3 | Environment="ETCD_CA_FILE=/etc/ssl/certs/{{ config.ca_cert_filename }}"
4 | Environment="ETCD_CERT_FILE=/etc/ssl/certs/{{ config.node_cert_filename }}"
5 | Environment="ETCD_KEY_FILE=/etc/ssl/certs/{{ config.node_key_filename }}"
6 | Environment="ETCD_PEER_CA_FILE=/etc/ssl/certs/{{ config.ca_cert_filename }}"
7 | Environment="ETCD_PEER_CERT_FILE=/etc/ssl/certs/{{ config.node_cert_filename }}"
8 | Environment="ETCD_PEER_KEY_FILE=/etc/ssl/certs/{{ config.node_key_filename }}"
9 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/flannel/__init__.py:
--------------------------------------------------------------------------------
1 | from seedbox import config
2 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
3 |
4 |
5 | class FlannelPackage(BaseIgnitionPackage):
6 | def get_files(self):
7 | files = []
8 | if self.cluster.k8s_cni:
9 | files += [{
10 | 'filesystem': 'root',
11 | 'path': config.k8s_cni_conf_path + '/10-flannel.conf',
12 | 'mode': 0o644,
13 | 'contents': {
14 | 'source': self.to_data_url(self.render_template('cni-conf.json')),
15 | },
16 | }]
17 | return files
18 |
19 | def get_units(self):
20 | dropins = [
21 | '40-etcd-cluster.conf',
22 | '40-network-config.conf',
23 | '40-oom.conf',
24 | ]
25 |
26 | if self.cluster.explicitly_advertise_addresses:
27 | dropins += [
28 | '40-iface.conf',
29 | ]
30 |
31 | if self.cluster.aci_proxy_url:
32 | dropins += [
33 | '30-proxy.conf',
34 | ]
35 |
36 | return [
37 | self.get_unit_dropins('flanneld.service', dropins=dropins),
38 | ]
39 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/flannel/cni-conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cbr0",
3 | "type": "flannel",
4 | "delegate": {
5 | "isDefaultGateway": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/flannel/flanneld.service.d/30-proxy.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment=HTTP_PROXY={{ cluster.aci_proxy_url }}
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/flannel/flanneld.service.d/40-etcd-cluster.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="FLANNELD_ETCD_ENDPOINTS={{ cluster.etcd_client_endpoints|join(',') }}"
3 | Environment="ETCD_SSL_DIR={{ config.cluster_credentials_path }}"
4 | Environment="FLANNELD_ETCD_CAFILE={{ config.ca_cert_path }}"
5 | Environment="FLANNELD_ETCD_CERTFILE={{ config.node_cert_path }}"
6 | Environment="FLANNELD_ETCD_KEYFILE={{ config.node_key_path }}"
7 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/flannel/flanneld.service.d/40-iface.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="FLANNELD_IFACE={{ node.ip }}"
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/flannel/flanneld.service.d/40-network-config.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | ExecStartPre=/opt/bin/cluster-etcdctl --timeout 30s \
3 | set /coreos.com/network/config '{"Network": "{{ cluster.k8s_pod_network }}", "Backend": {"Type": "vxlan"}}'
4 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/flannel/flanneld.service.d/40-oom.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | OOMScoreAdjust=-999
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/k8s_master_manifests/__init__.py:
--------------------------------------------------------------------------------
1 | from seedbox import config
2 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
3 |
4 |
5 | class K8sMasterManifestsPackage(BaseIgnitionPackage):
6 | def get_files(self):
7 | return [
8 | {
9 | 'filesystem': 'root',
10 | 'path': config.k8s_service_account_public_key_path,
11 | 'mode': 0o444,
12 | 'contents': {
13 | 'source': self.to_data_url(self.cluster.k8s_service_account_public_key),
14 | },
15 | },
16 | {
17 | 'filesystem': 'root',
18 | 'path': config.k8s_service_account_private_key_path,
19 | 'mode': 0o444,
20 | 'contents': {
21 | 'source': self.to_data_url(self.cluster.k8s_service_account_private_key),
22 | },
23 | },
24 | {
25 | 'filesystem': 'root',
26 | 'path': config.k8s_manifests_path + '/kube-apiserver.yaml',
27 | 'mode': 0o644,
28 | 'contents': {
29 | 'source': self.to_data_url(self.render_template('kube-apiserver.yaml')),
30 | },
31 | },
32 | {
33 | 'filesystem': 'root',
34 | 'path': config.k8s_manifests_path + '/kube-controller-manager.yaml',
35 | 'mode': 0o644,
36 | 'contents': {
37 | 'source': self.to_data_url(self.render_template('kube-controller-manager.yaml')),
38 | },
39 | },
40 | {
41 | 'filesystem': 'root',
42 | 'path': config.k8s_manifests_path + '/kube-scheduler.yaml',
43 | 'mode': 0o644,
44 | 'contents': {
45 | 'source': self.to_data_url(self.render_template('kube-scheduler.yaml')),
46 | },
47 | },
48 | ]
49 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/k8s_master_manifests/kube-apiserver.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: kube-apiserver
5 | namespace: kube-system
6 | spec:
7 | hostNetwork: true
8 | containers:
9 | - name: kube-apiserver
10 | image: {{ config.k8s_hyperkube_image }}:{{ cluster.k8s_hyperkube_tag }}
11 | command:
12 | - /hyperkube
13 | - apiserver
14 | - --bind-address=0.0.0.0
15 | {% if cluster.explicitly_advertise_addresses -%}
16 | - --advertise-address={{ node.ip }}
17 | {% endif -%}
18 | - --secure-port={{ config.k8s_apiserver_secure_port }}
19 | - --insecure-port={{ config.k8s_apiserver_insecure_port }}
20 | - --etcd-servers={{ cluster.etcd_client_endpoints|join(',') }}
21 | - --etcd-cafile={{ config.ca_cert_path }}
22 | - --etcd-certfile={{ config.node_cert_path }}
23 | - --etcd-keyfile={{ config.node_key_path }}
24 | - --storage-backend=etcd3
25 | - --allow-privileged=true
26 | - --service-cluster-ip-range={{ cluster.k8s_service_network }}
27 | - --admission-control={{ cluster.k8s_admission_control }}
28 | - --service-account-key-file={{ config.k8s_service_account_public_key_path }}
29 | {% if cluster.k8s_apiservers_audit_log -%}
30 | - --audit-log-path=/dev/stdout
31 | {% endif -%}
32 | {% if cluster.k8s_apiservers_swagger_ui -%}
33 | - --enable-swagger-ui
34 | {% endif -%}
35 | - --anonymous-auth=false # * -> apiserver
36 | - --tls-cert-file={{ config.node_cert_path }} # * -> apiserver, auth cert
37 | - --tls-private-key-file={{ config.node_key_path }} # * -> apiserver, sign key
38 | - --client-ca-file={{ config.ca_cert_path }} # * -> apiserver, ca
39 | - --kubelet-client-certificate={{ config.node_cert_path }} # apiserver -> kubelet, auth cert
40 | - --kubelet-client-key={{ config.node_key_path }} # apiserver -> kubelet, sign key
41 | {% if cluster.k8s_is_rbac_enabled -%}
42 | - --authorization-mode=RBAC
43 | {% else -%}
44 | - --authorization-mode=AlwaysAllow
45 | {% endif -%}
46 | livenessProbe:
47 | httpGet:
48 | host: 127.0.0.1
49 | port: 8080
50 | path: /healthz
51 | initialDelaySeconds: 15
52 | timeoutSeconds: 15
53 | volumeMounts:
54 | - mountPath: {{ config.cluster_credentials_path }}
55 | name: ssl-certs-cluster
56 | readOnly: true
57 | - mountPath: /etc/ssl/certs
58 | name: ssl-certs-host
59 | readOnly: true
60 | - mountPath: {{ config.k8s_secrets_path }}
61 | name: k8s-secrets
62 | readOnly: true
63 | resources:
64 | limits:
65 | cpu: 200m
66 | memory: 256Mi
67 | volumes:
68 | - hostPath:
69 | path: {{ config.cluster_credentials_path }}
70 | name: ssl-certs-cluster
71 | - hostPath:
72 | path: {{ config.node_ca_certificates_path }}
73 | name: ssl-certs-host
74 | - hostPath:
75 | path: {{ config.k8s_secrets_path }}
76 | name: k8s-secrets
77 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/k8s_master_manifests/kube-controller-manager.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: kube-controller-manager
5 | namespace: kube-system
6 | spec:
7 | hostNetwork: true
8 | containers:
9 | - name: kube-controller-manager
10 | image: {{ config.k8s_hyperkube_image }}:{{ cluster.k8s_hyperkube_tag }}
11 | command:
12 | - /hyperkube
13 | - controller-manager
14 | - --master=http://127.0.0.1:{{ config.k8s_apiserver_insecure_port }}
15 | - --root-ca-file={{ config.ca_cert_path }}
16 | - --service-account-private-key-file={{ config.k8s_service_account_private_key_path }}
17 | - --leader-elect=true
18 | resources:
19 | limits:
20 | cpu: 200m
21 | memory: 100Mi
22 | livenessProbe:
23 | httpGet:
24 | host: 127.0.0.1
25 | path: /healthz
26 | port: 10252
27 | initialDelaySeconds: 15
28 | timeoutSeconds: 15
29 | volumeMounts:
30 | - mountPath: {{ config.cluster_credentials_path }}
31 | name: ssl-certs-cluster
32 | readOnly: true
33 | - mountPath: /etc/ssl/certs
34 | name: ssl-certs-host
35 | readOnly: true
36 | - mountPath: {{ config.k8s_secrets_path }}
37 | name: k8s-secrets
38 | readOnly: true
39 | volumes:
40 | - hostPath:
41 | path: {{ config.cluster_credentials_path }}
42 | name: ssl-certs-cluster
43 | - hostPath:
44 | path: {{ config.node_ca_certificates_path }}
45 | name: ssl-certs-host
46 | - hostPath:
47 | path: {{ config.k8s_secrets_path }}
48 | name: k8s-secrets
49 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/k8s_master_manifests/kube-scheduler.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: kube-scheduler
5 | namespace: kube-system
6 | spec:
7 | hostNetwork: true
8 | containers:
9 | - name: kube-scheduler
10 | image: {{ config.k8s_hyperkube_image }}:{{ cluster.k8s_hyperkube_tag }}
11 | command:
12 | - /hyperkube
13 | - scheduler
14 | - --master=http://127.0.0.1:{{ config.k8s_apiserver_insecure_port }}
15 | - --leader-elect=true
16 | resources:
17 | limits:
18 | cpu: 200m
19 | memory: 50Mi
20 | livenessProbe:
21 | httpGet:
22 | host: 127.0.0.1
23 | path: /healthz
24 | port: 10251
25 | initialDelaySeconds: 15
26 | timeoutSeconds: 15
27 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/kube_proxy/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from seedbox import config, config_renderer
4 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
5 |
6 | log = logging.getLogger(__name__)
7 |
8 |
9 | class KubeProxyPackage(BaseIgnitionPackage):
10 | def get_files(self):
11 | user = self.cluster.k8s_kube_proxy_user
12 | if not user:
13 | log.warning('No user "%s" for kube-proxy in cluster %s', config.k8s_kube_proxy_user_name, self.cluster)
14 | return []
15 |
16 | return [
17 | {
18 | 'filesystem': 'root',
19 | 'path': config.k8s_manifests_path + '/kube-proxy.yaml',
20 | 'mode': 0o644,
21 | 'contents': {
22 | 'source': self.to_data_url(self.render_template('kube-proxy.yaml')),
23 | },
24 | },
25 | {
26 | 'filesystem': 'root',
27 | 'path': config.k8s_kube_proxy_config_path,
28 | 'mode': 0o644,
29 | 'contents': {
30 | 'source': self.to_data_url(config_renderer.kubeconfig.render([user], default_user=user)),
31 | },
32 | },
33 | ]
34 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/kube_proxy/kube-proxy.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: kube-proxy
5 | namespace: kube-system
6 | spec:
7 | hostNetwork: true
8 | containers:
9 | - name: kube-proxy
10 | image: {{ config.k8s_hyperkube_image }}:{{ cluster.k8s_hyperkube_tag }}
11 | command:
12 | - /hyperkube
13 | - proxy
14 | - --kubeconfig=/etc/kubeconfig.yaml
15 | securityContext:
16 | privileged: true
17 | volumeMounts:
18 | - mountPath: /etc/ssl/certs
19 | name: ssl-certs-host
20 | readOnly: true
21 | - mountPath: {{ config.cluster_credentials_path }}
22 | name: ssl-certs-cluster
23 | readOnly: true
24 | - mountPath: /etc/kubeconfig.yaml
25 | name: kubeconfig
26 | readOnly: true
27 | volumes:
28 | - hostPath:
29 | path: {{ config.node_ca_certificates_path }}
30 | name: ssl-certs-host
31 | - hostPath:
32 | path: {{ config.cluster_credentials_path }}
33 | name: ssl-certs-cluster
34 | - hostPath:
35 | path: {{ config.k8s_kube_proxy_config_path }}
36 | name: kubeconfig
37 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/kubeconfig/__init__.py:
--------------------------------------------------------------------------------
1 | from seedbox import config
2 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
3 |
4 |
5 | class KubeconfigPackage(BaseIgnitionPackage):
6 | def get_files(self):
7 | return [
8 | {
9 | 'filesystem': 'root',
10 | 'path': config.k8s_kubeconfig_path,
11 | 'mode': 0o644,
12 | 'contents': {
13 | 'source': self.to_data_url(self.render_template('kubeconfig.yaml')),
14 | },
15 | },
16 | ]
17 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/kubeconfig/kubeconfig.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Config
3 | clusters:
4 | - name: {{ cluster.name }}
5 | cluster:
6 | server: {{ cluster.k8s_apiserver_endpoint }}
7 | certificate-authority: {{ config.ca_cert_path }}
8 | users:
9 | - name: kubelet
10 | user:
11 | client-certificate: {{ config.node_cert_path }}
12 | client-key: {{ config.node_key_path }}
13 | contexts:
14 | - context:
15 | cluster: {{ cluster.name }}
16 | user: kubelet
17 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/kubelet/__init__.py:
--------------------------------------------------------------------------------
1 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
2 |
3 |
4 | class KubeletPackage(BaseIgnitionPackage):
5 | def get_units(self):
6 | units = [
7 | self.get_unit('kubelet.service', enable=True),
8 | ]
9 |
10 | if self.cluster.aci_proxy_url:
11 | units += [
12 | self.get_unit_dropins('kubelet.service', ['30-proxy.conf']),
13 | ]
14 |
15 | return units
16 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/kubelet/kubelet.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Kubelet via Hyperkube ACI
3 | Wants=flanneld.service
4 |
5 | [Service]
6 | Environment="KUBELET_ACI={{ config.k8s_hyperkube_image }}"
7 | Environment="KUBELET_IMAGE_TAG={{ cluster.k8s_hyperkube_tag }}"
8 | Environment="RKT_RUN_ARGS=--uuid-file-save=/var/run/kubelet-pod.uuid \
9 | --volume resolv,kind=host,source=/etc/resolv.conf \
10 | --mount volume=resolv,target=/etc/resolv.conf \
11 | --volume ssl-cluster-credentials,kind=host,source={{ config.cluster_credentials_path }},readOnly=true \
12 | --mount volume=ssl-cluster-credentials,target={{ config.cluster_credentials_path }} \
13 | --volume var-log,kind=host,source=/var/log \
14 | --mount volume=var-log,target=/var/log \
15 | {% if cluster.k8s_cni -%}
16 | --volume var-lib-cni,kind=host,source=/var/lib/cni \
17 | --mount volume=var-lib-cni,target=/var/lib/cni \
18 | {% endif -%}
19 | --volume modprobe,kind=host,source=/usr/sbin/modprobe \
20 | --mount volume=modprobe,target=/usr/sbin/modprobe \
21 | --volume lib-modules,kind=host,source=/lib/modules \
22 | --mount volume=lib-modules,target=/lib/modules \
23 | --volume docker-config,kind=host,source=/root/.docker,readOnly=true \
24 | --mount volume=docker-config,target=/.docker \
25 | "
26 |
27 | ExecStartPre=/usr/bin/mkdir -p {{ config.k8s_manifests_path }}
28 | ExecStartPre=/usr/bin/mkdir -p /var/log/containers
29 | {% if cluster.k8s_cni -%}
30 | ExecStartPre=/usr/bin/mkdir -p /var/lib/cni
31 | {% endif -%}
32 | ExecStartPre=/usr/bin/systemctl is-active flanneld.service
33 | ExecStartPre=-/usr/bin/rkt rm --uuid-file=/var/run/kubelet-pod.uuid
34 |
35 | # --client-ca-file and --anonymous-auth arguments are for apiserver -> kubelet communication
36 | ExecStart=/usr/lib/coreos/kubelet-wrapper \
37 | --kubeconfig={{ config.k8s_kubeconfig_path }} \
38 | --require-kubeconfig \
39 | --client-ca-file={{ config.ca_cert_path }} \
40 | --anonymous-auth=false \
41 | {% if cluster.k8s_cni -%}
42 | --cni-conf-dir={{ config.k8s_cni_conf_path }} \
43 | --network-plugin=cni \
44 | {% endif -%}
45 | --pod-manifest-path={{ config.k8s_manifests_path }} \
46 | --allow-privileged=true \
47 | --hostname-override={{ node.fqdn }} \
48 | {% if node.is_k8s_master -%}
49 | --node-labels=node-role.kubernetes.io/master \
50 | {% else -%}
51 | --node-labels=node-role.kubernetes.io/node \
52 | {% endif -%}
53 | --cluster-dns={{ cluster.k8s_dns_service_ip }} \
54 | --cluster-domain={{ config.k8s_cluster_domain }} \
55 | {% if node.is_k8s_schedulable -%}
56 | --register-schedulable=true \
57 | {% else %}
58 | --register-schedulable=false \
59 | {% endif -%}
60 | \
61 |
62 | ExecStop=-/usr/bin/rkt stop --uuid-file=/var/run/kubelet-pod.uuid
63 |
64 | Restart=always
65 | RestartSec=10
66 |
67 | [Install]
68 | WantedBy=multi-user.target
69 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/kubelet/kubelet.service.d/30-proxy.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment=HTTP_PROXY={{ cluster.aci_proxy_url }}
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/__init__.py:
--------------------------------------------------------------------------------
1 | from types import SimpleNamespace
2 | from collections import defaultdict
3 |
4 | from seedbox import config
5 | from seedbox.config_renderer.ignition.base import BaseIgnitionPackage
6 |
7 |
8 | class SystemPackage(BaseIgnitionPackage):
9 | def get_files(self):
10 | files = [
11 | {
12 | 'filesystem': 'root',
13 | 'path': '/etc/sysctl.d/max-user-watches.conf',
14 | 'mode': 0o644,
15 | 'contents': {
16 | 'source': self.to_data_url(self.render_template('sysctl-max-user-watches.conf')),
17 | },
18 | },
19 | {
20 | 'filesystem': 'root',
21 | 'path': '/etc/hostname',
22 | 'mode': 0o644,
23 | 'contents': {
24 | 'source': self.to_data_url(self.node.fqdn + '\n'),
25 | },
26 | },
27 | {
28 | 'filesystem': 'root',
29 | 'path': '/opt/bin/cluster-etcdctl',
30 | 'mode': 0o755,
31 | 'contents': {
32 | 'source': self.to_data_url(self.render_template('cluster-etcdctl')),
33 | },
34 | },
35 | {
36 | 'filesystem': 'root',
37 | 'path': '/root/.docker/config.json',
38 | 'mode': 0o600,
39 | 'contents': {
40 | 'source': self.to_data_url(self.cluster.docker_config),
41 | },
42 | },
43 | ]
44 |
45 | if self.cluster.aci_proxy_ca_cert:
46 | files += [
47 | {
48 | 'filesystem': 'root',
49 | 'path': config.aci_proxy_ca_cert_path,
50 | 'mode': 0o644,
51 | 'contents': {
52 | 'source': self.to_data_url(self.cluster.aci_proxy_ca_cert + '\n'),
53 | },
54 | },
55 | ]
56 |
57 | return files
58 |
59 | def get_units(self):
60 | units = [
61 | self.get_unit('provision-report.service', enable=True),
62 | self.get_unit_dropins('fleet.service', ['40-etcd-cluster.conf']),
63 | self.get_unit_dropins('locksmithd.service', [
64 | '40-etcd-cluster.conf',
65 | '40-etcd-lock.conf',
66 | ]),
67 | self.get_unit_dropins('sshd.service', ['40-oom.conf']),
68 | self.get_unit_dropins('sshd@.service', ['40-oom.conf']),
69 | self.get_unit_dropins('containerd.service', ['40-oom.conf']),
70 | ]
71 |
72 | if self.cluster.aci_proxy_url:
73 | units += [
74 | self.get_unit_dropins('docker.service', ['30-proxy.conf']),
75 | ]
76 |
77 | if self.cluster.aci_proxy_ca_cert:
78 | units += [
79 | self.get_unit('add-http-proxy-ca-certificate.service', enable=True)
80 | ]
81 |
82 | for mountpoint in self.node.mountpoints.all():
83 | units += [
84 | self.get_unit(mountpoint2unitname(mountpoint),
85 | enable=bool(mountpoint.wanted_by),
86 | template_name='volume.mount',
87 | additional_context={
88 | 'mountpoint': mountpoint,
89 | })
90 | ]
91 |
92 | return units
93 |
94 | def get_networkd_units(self):
95 | interfaces = defaultdict(list)
96 | for address in self.node.addresses.all():
97 | interfaces[address.interface].append(address)
98 |
99 | return [
100 | self.get_unit('aa-{}-addresses.network'.format(interface),
101 | template_name='addresses.network',
102 | additional_context={
103 | 'interface': interface,
104 | 'addresses': addresses,
105 | })
106 | for interface, addresses in interfaces.items()
107 | ]
108 |
109 |
110 | def mountpoint2unitname(mountpoint):
111 | unit_name = mountpoint.where.replace('/', '-')
112 | while unit_name[0] == '-':
113 | unit_name = unit_name[1:]
114 | return unit_name + '.mount'
115 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/add-http-proxy-ca-certificate.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Add http proxy CA certificate to /etc/ssl/certs/ca-certificates.crt
3 | After=update-ca-certificates.service
4 |
5 | [Service]
6 | Type=oneshot
7 | ExecStartPre=/bin/rm /etc/ssl/certs/ca-certificates.crt
8 | ExecStart=/bin/sh -c 'cat /usr/share/ca-certificates/ca-certificates.crt {{ config.aci_proxy_ca_cert_path }} > /etc/ssl/certs/ca-certificates.crt'
9 |
10 | [Install]
11 | WantedBy=update-ca-certificates.service
12 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/addresses.network:
--------------------------------------------------------------------------------
1 | [Match]
2 | Name={{ interface }}
3 |
4 | [Network]
5 | DHCP=yes
6 | {% for addr in addresses -%}
7 | Address={{ addr.ip }}/32
8 | {% endfor %}
9 |
10 | [DHCP]
11 | UseMTU=true
12 | UseDomains=true
13 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/cluster-etcdctl:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | # etcd-wrapper options
5 | export ETCD_IMAGE_TAG={{ cluster.etcd_image_tag }}
6 | export ETCD_USER=etcd
7 | export ETCD_DATA_DIR=/tmp/etdctl
8 | export ETCD_SSL_DIR={{ config.cluster_credentials_path }}
9 | export ETCD_IMAGE_ARGS='--exec /usr/bin/env'
10 |
11 | # used for ETCDCTL_API=2
12 | export ETCDCTL_CA_FILE=/etc/ssl/certs/{{ config.ca_cert_filename }}
13 | export ETCDCTL_CERT_FILE=/etc/ssl/certs/{{ config.node_cert_filename }}
14 | export ETCDCTL_KEY_FILE=/etc/ssl/certs/{{ config.node_key_filename }}
15 |
16 | # used for ETCDCTL_API=3
17 | export ETCDCTL_CACERT=$ETCDCTL_CA_FILE
18 | export ETCDCTL_CERT=$ETCDCTL_CERT_FILE
19 | export ETCDCTL_KEY=$ETCDCTL_KEY_FILE
20 |
21 | export ETCDCTL_ENDPOINTS={{ cluster.etcd_client_endpoints|join(',') }}
22 |
23 | exec /usr/lib/coreos/etcd-wrapper etcdctl "$@"
24 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/containerd.service.d/40-oom.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | OOMScoreAdjust=-999
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/docker.service.d/30-proxy.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | # works only to fetch images
3 | Environment=HTTP_PROXY={{ cluster.aci_proxy_url }}
4 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/fleet.service.d/40-etcd-cluster.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="FLEET_ETCD_SERVERS={{ cluster.etcd_client_endpoints|join(',') }}"
3 | Environment="FLEET_ETCD_CAFILE={{ config.ca_cert_path }}"
4 | Environment="FLEET_ETCD_CERTFILE={{ config.node_cert_path }}"
5 | Environment="FLEET_ETCD_KEYFILE={{ config.node_key_path }}"
6 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/locksmithd.service.d/40-etcd-cluster.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="LOCKSMITHD_ENDPOINT={{ cluster.etcd_client_endpoints|join(',') }}"
3 | Environment="LOCKSMITHD_ETCD_CAFILE={{ config.ca_cert_path }}"
4 | Environment="LOCKSMITHD_ETCD_CERTFILE={{ config.node_cert_path }}"
5 | Environment="LOCKSMITHD_ETCD_KEYFILE={{ config.node_key_path }}"
6 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/locksmithd.service.d/40-etcd-lock.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | Environment="REBOOT_STRATEGY=etcd-lock"
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/provision-report.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Provision Report
3 | Requires=network-online.target
4 | After=network-online.target
5 |
6 | [Service]
7 | Type=oneshot
8 | ExecStart=/usr/bin/curl --fail \
9 | --silent \
10 | --show-error \
11 | --location \
12 | -H 'Content-Type: application/json' \
13 | -X POST \
14 | --data-binary @/run/ignition.json \
15 | '{{ url_root }}report?version={{ node.target_config_version }}'
16 |
17 | [Install]
18 | WantedBy=multi-user.target
19 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/sshd.service.d/40-oom.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | OOMScoreAdjust=-1000
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/sshd@.service.d/40-oom.conf:
--------------------------------------------------------------------------------
1 | [Service]
2 | OOMScoreAdjust=-1000
3 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/sysctl-max-user-watches.conf:
--------------------------------------------------------------------------------
1 | fs.inotify.max_user_watches=16184
2 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ignition/system/volume.mount:
--------------------------------------------------------------------------------
1 | [Mount]
2 | What={{ mountpoint.what }}
3 | Where={{ mountpoint.where }}
4 |
5 | [Install]
6 | WantedBy={{ mountpoint.wanted_by }}
7 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/ipxe.py:
--------------------------------------------------------------------------------
1 | response_template = """#!ipxe
2 | set base-url {images_base_url}
3 | kernel ${{base-url}}coreos_production_pxe.vmlinuz {kernel_args}
4 | initrd ${{base-url}}coreos_production_pxe_image.cpio.gz
5 | boot
6 | """
7 |
8 |
9 | def render(node, url_root):
10 | kernel_args = get_kernel_arguments(node, url_root)
11 | return response_template.format(images_base_url=node.cluster.coreos_images_base_url,
12 | kernel_args=' '.join(kernel_args))
13 |
14 |
15 | def get_kernel_arguments(node, url_root):
16 | args = [
17 | 'coreos.config.url={}ignition'.format(url_root),
18 | 'coreos.first_boot=yes',
19 | ]
20 |
21 | if node.coreos_autologin:
22 | args.append('coreos.autologin')
23 |
24 | for console in node.linux_consoles.split(','):
25 | args.append('console=' + console)
26 |
27 | if not node.maintenance_mode:
28 | args.append('root=' + node.root_partition.device)
29 |
30 | if node.disable_ipv6:
31 | args.append('ipv6.disable=1')
32 |
33 | if node.debug_boot:
34 | args += [
35 | 'systemd.journald.forward_to_kmsg=1',
36 | 'systemd.journald.max_level_kmsg=debug',
37 | ]
38 |
39 | if node.additional_kernel_cmdline:
40 | args += node.additional_kernel_cmdline.split()
41 |
42 | return args
43 |
--------------------------------------------------------------------------------
/src/seedbox/config_renderer/kubeconfig.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from collections import OrderedDict
3 |
4 | import yaml
5 | import yaml.resolver
6 |
7 |
8 | def render(users, default_user=None):
9 | clusters_map = {}
10 | users_map = {}
11 |
12 | for user in users:
13 | users_map[user.name] = user
14 | clusters_map[user.cluster.name] = user.cluster
15 |
16 | contexts = [{
17 | 'name': user.name,
18 | 'context': {
19 | 'cluster': user.cluster.name,
20 | 'user': user.name,
21 | },
22 | } for user in users_map.values()]
23 |
24 | clusters = [{
25 | 'name': cluster.name,
26 | 'cluster': {
27 | 'server': cluster.k8s_apiserver_endpoint,
28 | 'certificate-authority-data': base64.b64encode(cluster.ca_credentials.cert).decode('ascii'),
29 | },
30 | } for cluster in clusters_map.values()]
31 |
32 | users = [{
33 | 'name': user.name,
34 | 'user': {
35 | 'client-certificate-data': base64.b64encode(user.credentials.cert).decode('ascii'),
36 | 'client-key-data': base64.b64encode(user.credentials.key).decode('ascii'),
37 | },
38 | } for user in users_map.values()]
39 |
40 | config = OrderedDict([
41 | ('apiVersion', 'v1'),
42 | ('kind', 'Config'),
43 | ('clusters', clusters),
44 | ('users', users),
45 | ('contexts', contexts),
46 | ])
47 |
48 | if default_user:
49 | config['current-context'] = default_user.name
50 |
51 | return yaml.dump(config, default_flow_style=False, Dumper=Dumper)
52 |
53 |
54 | class Dumper(yaml.SafeDumper):
55 | pass
56 |
57 |
58 | def _dict_representer(dumper, data):
59 | return dumper.represent_mapping(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, data.items())
60 |
61 |
62 | Dumper.add_representer(OrderedDict, _dict_representer)
63 |
--------------------------------------------------------------------------------
/src/seedbox/exceptions.py:
--------------------------------------------------------------------------------
1 | class K8sNoClusterApiserver(Exception):
2 | def __str__(self):
3 | return "No node with k8s apiserver"
4 |
5 |
6 | class NoRootPartition(Exception):
7 | def __str__(self):
8 | return "No partition labeled ROOT defined"
9 |
10 |
11 | class MultipleRootPartitions(Exception):
12 | def __str__(self):
13 | return "More than one partition labeled ROOT defined"
14 |
15 |
16 | class MultiplePersistentMountpoints(Exception):
17 | def __str__(self):
18 | return "More than one persistent mountpoint defined"
19 |
--------------------------------------------------------------------------------
/src/seedbox/ignition_parser.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 | import json
4 | import urllib.parse
5 |
6 | from seedbox import utils
7 |
8 | encoding = 'utf-8'
9 |
10 |
11 | def iter_files(ignition):
12 | if 'storage' in ignition and 'files' in ignition['storage']:
13 | files = ignition['storage'].pop('files')
14 |
15 | for file in files:
16 | file_path = file['path']
17 | if file_path[0] == '/':
18 | file_path = file_path[1:]
19 | file_path = os.path.join(file['filesystem'], file_path)
20 |
21 | file_mode = file.get('mode')
22 | file_uid = file.get('user', {}).get('id')
23 | file_gid = file.get('group', {}).get('id')
24 |
25 | file_attrs = b''
26 | if file_mode is not None:
27 | file_attrs += b'mode: %s\n' % oct(int(file_mode))[2:].encode(encoding)
28 | if file_uid is not None:
29 | file_attrs += b'uid: %s\n' % file_uid
30 | if file_gid is not None:
31 | file_attrs += b'gid: %s\n' % file_gid
32 |
33 | if file_attrs:
34 | yield file_path + '.attrs', file_attrs
35 |
36 | file_source = file['contents']['source']
37 | if file_source.startswith('data:,'):
38 | file_data = urllib.parse.unquote(file_source[len('data:,'):]).encode(encoding)
39 | else:
40 | file_path += '.src'
41 | file_data = file_source.encode(encoding) + b'\n'
42 |
43 | yield file_path, file_data
44 |
45 | if 'systemd' in ignition and 'units' in ignition['systemd']:
46 | units = ignition['systemd'].pop('units')
47 | units_dir_path = os.path.join('root', 'etc', 'systemd', 'system')
48 |
49 | for unit in units:
50 | unitname = unit['name']
51 | unit_file_path = os.path.join(units_dir_path, unitname)
52 |
53 | if unit.get('enable'):
54 | yield unit_file_path + '.enabled', b''
55 |
56 | if 'contents' in unit:
57 | yield unit_file_path, unit['contents'].encode(encoding)
58 |
59 | if 'dropins' in unit:
60 | unit_dropins_path = os.path.join(units_dir_path, unitname + '.d')
61 | for dropin in unit['dropins']:
62 | yield os.path.join(unit_dropins_path, dropin['name']), dropin['contents'].encode(encoding)
63 |
64 | yield 'non-fs.json', json.dumps(ignition, indent=2).encode(encoding)
65 |
66 |
67 | def render_ignition_tgz(ignition):
68 | tgz_fp = io.BytesIO()
69 | with utils.TarFile.open(fileobj=tgz_fp, mode='w:gz') as tgz:
70 | for path, data in iter_files(ignition):
71 | tgz.adddata(os.path.join('ignition', path), data)
72 | return tgz_fp.getvalue()
73 |
--------------------------------------------------------------------------------
/src/seedbox/manage.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from flask_script import Manager
4 | from flask_migrate import MigrateCommand
5 |
6 | from seedbox.app import app
7 |
8 | manager = Manager(app)
9 | manager.add_command('db', MigrateCommand)
10 |
11 |
12 | @manager.command
13 | def watch_updates():
14 | """Starts component updates watcher in foreground (CoreOS, k8s, etcd)"""
15 | from seedbox import update_watcher
16 | update_watcher.watch()
17 |
18 |
19 | def run():
20 | logging.basicConfig(level='NOTSET')
21 | manager.run()
22 |
23 |
24 | if __name__ == '__main__':
25 | run()
26 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/src/seedbox/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/src/seedbox/migrations/__init__.py
--------------------------------------------------------------------------------
/src/seedbox/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from alembic import context
3 | from sqlalchemy import engine_from_config, pool
4 | from logging.config import fileConfig
5 | import logging
6 |
7 | # this is the Alembic Config object, which provides
8 | # access to the values within the .ini file in use.
9 | config = context.config
10 |
11 | # Interpret the config file for Python logging.
12 | # This line sets up loggers basically.
13 | fileConfig(config.config_file_name)
14 | logger = logging.getLogger('alembic.env')
15 |
16 | # add your model's MetaData object here
17 | # for 'autogenerate' support
18 | # from myapp import mymodel
19 | # target_metadata = mymodel.Base.metadata
20 | from flask import current_app
21 | config.set_main_option('sqlalchemy.url',
22 | current_app.config.get('SQLALCHEMY_DATABASE_URI'))
23 | target_metadata = current_app.extensions['migrate'].db.metadata
24 |
25 | # other values from the config, defined by the needs of env.py,
26 | # can be acquired:
27 | # my_important_option = config.get_main_option("my_important_option")
28 | # ... etc.
29 |
30 |
31 | def run_migrations_offline():
32 | """Run migrations in 'offline' mode.
33 |
34 | This configures the context with just a URL
35 | and not an Engine, though an Engine is acceptable
36 | here as well. By skipping the Engine creation
37 | we don't even need a DBAPI to be available.
38 |
39 | Calls to context.execute() here emit the given string to the
40 | script output.
41 |
42 | """
43 | url = config.get_main_option("sqlalchemy.url")
44 | context.configure(url=url)
45 |
46 | with context.begin_transaction():
47 | context.run_migrations()
48 |
49 |
50 | def run_migrations_online():
51 | """Run migrations in 'online' mode.
52 |
53 | In this scenario we need to create an Engine
54 | and associate a connection with the context.
55 |
56 | """
57 |
58 | # this callback is used to prevent an auto-migration from being generated
59 | # when there are no changes to the schema
60 | # reference: http://alembic.readthedocs.org/en/latest/cookbook.html
61 | def process_revision_directives(context, revision, directives):
62 | if getattr(config.cmd_opts, 'autogenerate', False):
63 | script = directives[0]
64 | if script.upgrade_ops.is_empty():
65 | directives[:] = []
66 | logger.info('No changes in schema detected.')
67 |
68 | engine = engine_from_config(config.get_section(config.config_ini_section),
69 | prefix='sqlalchemy.',
70 | poolclass=pool.NullPool)
71 |
72 | connection = engine.connect()
73 | context.configure(connection=connection,
74 | target_metadata=target_metadata,
75 | process_revision_directives=process_revision_directives,
76 | **current_app.extensions['migrate'].configure_args)
77 |
78 | try:
79 | with context.begin_transaction():
80 | context.run_migrations()
81 | finally:
82 | connection.close()
83 |
84 | if context.is_offline_mode():
85 | run_migrations_offline()
86 | else:
87 | run_migrations_online()
88 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/028018cd8818_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 028018cd8818
4 | Revises: 25fc3f52af44
5 | Create Date: 2017-04-10 23:21:49.523249
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '028018cd8818'
14 | down_revision = '25fc3f52af44'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('cluster', sa.Column('dnsmasq_static_records', sa.Boolean(), nullable=True))
22 | op.execute('UPDATE cluster SET dnsmasq_static_records=install_dnsmasq')
23 | op.alter_column('cluster', 'dnsmasq_static_records', nullable=False)
24 | # ### end Alembic commands ###
25 |
26 |
27 | def downgrade():
28 | # ### commands auto generated by Alembic - please adjust! ###
29 | op.drop_column('cluster', 'dnsmasq_static_records')
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/03314dc2f789_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 03314dc2f789
4 | Revises: 235f683efac0
5 | Create Date: 2017-06-19 15:39:34.872360
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '03314dc2f789'
14 | down_revision = '235f683efac0'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('cluster', sa.Column('docker_config', sa.Text(), nullable=False, server_default='{}'))
22 | op.alter_column('cluster', 'docker_config', server_default=None)
23 | # ### end Alembic commands ###
24 |
25 |
26 | def downgrade():
27 | # ### commands auto generated by Alembic - please adjust! ###
28 | op.drop_column('cluster', 'docker_config')
29 | # ### end Alembic commands ###
30 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/2093be28531b_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 2093be28531b
4 | Revises: da3084151a1d
5 | Create Date: 2017-04-28 18:55:40.347204
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '2093be28531b'
14 | down_revision = 'da3084151a1d'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('node', sa.Column('additional_kernel_cmdline', sa.String(length=255), nullable=False,
22 | server_default=''))
23 | op.alter_column('node', 'additional_kernel_cmdline', server_default=None)
24 | # ### end Alembic commands ###
25 |
26 |
27 | def downgrade():
28 | # ### commands auto generated by Alembic - please adjust! ###
29 | op.drop_column('node', 'additional_kernel_cmdline')
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/235f683efac0_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 235f683efac0
4 | Revises: 35099fc974d2
5 | Create Date: 2017-05-19 18:46:59.356128
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '235f683efac0'
14 | down_revision = '35099fc974d2'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_unique_constraint('_node_mountpoint_what_uc', 'mountpoint', ['node_id', 'what'])
22 | op.create_unique_constraint('_node_mountpoint_where_uc', 'mountpoint', ['node_id', 'where'])
23 | # ### end Alembic commands ###
24 |
25 |
26 | def downgrade():
27 | # ### commands auto generated by Alembic - please adjust! ###
28 | op.drop_constraint('_node_mountpoint_where_uc', 'mountpoint', type_='unique')
29 | op.drop_constraint('_node_mountpoint_what_uc', 'mountpoint', type_='unique')
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/25fc3f52af44_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 25fc3f52af44
4 | Revises:
5 | Create Date: 2017-04-10 17:15:01.049591
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '25fc3f52af44'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('credentials_data',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('cert', sa.Binary(), nullable=False),
24 | sa.Column('key', sa.Binary(), nullable=False),
25 | sa.PrimaryKeyConstraint('id')
26 | )
27 | op.create_table('cluster',
28 | sa.Column('id', sa.Integer(), nullable=False),
29 | sa.Column('name', sa.String(length=80), nullable=False),
30 | sa.Column('ca_credentials_id', sa.Integer(), nullable=False),
31 | sa.Column('etcd_version', sa.Integer(), nullable=False),
32 | sa.Column('suppose_etcd_cluster_exists', sa.Boolean(), nullable=False),
33 | sa.Column('etcd_nodes_dns_name', sa.String(length=80), nullable=False),
34 | sa.Column('install_dnsmasq', sa.Boolean(), nullable=False),
35 | sa.Column('allow_insecure_provision', sa.Boolean(), nullable=False),
36 | sa.Column('apiservers_audit_log', sa.Boolean(), nullable=False),
37 | sa.Column('apiservers_swagger_ui', sa.Boolean(), nullable=False),
38 | sa.Column('explicitly_advertise_addresses', sa.Boolean(), nullable=False),
39 | sa.Column('k8s_pod_network', sa.String(length=80), nullable=False),
40 | sa.Column('k8s_service_network', sa.String(length=80), nullable=False),
41 | sa.Column('k8s_hyperkube_tag', sa.String(length=80), nullable=False),
42 | sa.Column('k8s_cni', sa.Boolean(), nullable=False),
43 | sa.Column('k8s_apiservers_dns_name', sa.String(length=80), nullable=False),
44 | sa.Column('boot_images_base_url', sa.String(length=80), nullable=False),
45 | sa.Column('aci_proxy_url', sa.String(length=80), nullable=False),
46 | sa.Column('aci_proxy_ca_cert', sa.Text(), nullable=False),
47 | sa.Column('service_account_keypair_id', sa.Integer(), nullable=False),
48 | sa.ForeignKeyConstraint(['ca_credentials_id'], ['credentials_data.id'], ),
49 | sa.ForeignKeyConstraint(['service_account_keypair_id'], ['credentials_data.id'], ),
50 | sa.PrimaryKeyConstraint('id'),
51 | sa.UniqueConstraint('name')
52 | )
53 | op.create_table('node',
54 | sa.Column('id', sa.Integer(), nullable=False),
55 | sa.Column('ip', sa.String(length=80), nullable=False),
56 | sa.Column('fqdn', sa.String(length=80), nullable=False),
57 | sa.Column('maintenance_mode', sa.Boolean(), nullable=False),
58 | sa.Column('cluster_id', sa.Integer(), nullable=False),
59 | sa.Column('credentials_id', sa.Integer(), nullable=False),
60 | sa.Column('target_config_version', sa.Integer(), nullable=False),
61 | sa.Column('active_config_version', sa.Integer(), nullable=True),
62 | sa.Column('active_ignition_config', sa.Text(), nullable=False),
63 | sa.Column('coreos_autologin', sa.Boolean(), nullable=False),
64 | sa.Column('root_disk', sa.String(length=80), nullable=False),
65 | sa.Column('wipe_root_disk_next_boot', sa.Boolean(), nullable=False),
66 | sa.Column('root_disk_size_sectors', sa.Integer(), nullable=True),
67 | sa.Column('linux_consoles', sa.String(length=80), nullable=False),
68 | sa.Column('disable_ipv6', sa.Boolean(), nullable=False),
69 | sa.Column('is_etcd_server', sa.Boolean(), nullable=False),
70 | sa.Column('is_k8s_schedulable', sa.Boolean(), nullable=False),
71 | sa.Column('is_k8s_master', sa.Boolean(), nullable=False),
72 | sa.ForeignKeyConstraint(['cluster_id'], ['cluster.id'], ),
73 | sa.ForeignKeyConstraint(['credentials_id'], ['credentials_data.id'], ),
74 | sa.PrimaryKeyConstraint('id'),
75 | sa.UniqueConstraint('fqdn'),
76 | sa.UniqueConstraint('ip')
77 | )
78 | op.create_table('user',
79 | sa.Column('id', sa.Integer(), nullable=False),
80 | sa.Column('cluster_id', sa.Integer(), nullable=False),
81 | sa.Column('name', sa.String(length=80), nullable=False),
82 | sa.Column('credentials_id', sa.Integer(), nullable=False),
83 | sa.Column('groups', sa.String(length=255), nullable=False),
84 | sa.Column('ssh_key', sa.Text(), nullable=False),
85 | sa.ForeignKeyConstraint(['cluster_id'], ['cluster.id'], ),
86 | sa.ForeignKeyConstraint(['credentials_id'], ['credentials_data.id'], ),
87 | sa.PrimaryKeyConstraint('id')
88 | )
89 | op.create_table('address',
90 | sa.Column('id', sa.Integer(), nullable=False),
91 | sa.Column('node_id', sa.Integer(), nullable=False),
92 | sa.Column('interface', sa.String(length=80), nullable=False),
93 | sa.Column('ip', sa.String(length=80), nullable=False),
94 | sa.ForeignKeyConstraint(['node_id'], ['node.id'], ),
95 | sa.PrimaryKeyConstraint('id')
96 | )
97 | op.create_table('mountpoint',
98 | sa.Column('id', sa.Integer(), nullable=False),
99 | sa.Column('node_id', sa.Integer(), nullable=False),
100 | sa.Column('what', sa.String(length=80), nullable=False),
101 | sa.Column('where', sa.String(length=80), nullable=False),
102 | sa.Column('wanted_by', sa.String(length=80), nullable=False),
103 | sa.ForeignKeyConstraint(['node_id'], ['node.id'], ),
104 | sa.PrimaryKeyConstraint('id')
105 | )
106 | # ### end Alembic commands ###
107 |
108 |
109 | def downgrade():
110 | # ### commands auto generated by Alembic - please adjust! ###
111 | op.drop_table('mountpoint')
112 | op.drop_table('address')
113 | op.drop_table('user')
114 | op.drop_table('node')
115 | op.drop_table('cluster')
116 | op.drop_table('credentials_data')
117 | # ### end Alembic commands ###
118 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/283dc9256301_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 283dc9256301
4 | Revises: 52e82e7e9376
5 | Create Date: 2017-05-01 22:10:11.649870
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '283dc9256301'
14 | down_revision = '52e82e7e9376'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('provision',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('node_id', sa.Integer(), nullable=False),
24 | sa.Column('applied_at', sa.DateTime(), nullable=True),
25 | sa.Column('config_version', sa.Integer(), nullable=False),
26 | sa.Column('ignition_config', sa.Binary(), nullable=True),
27 | sa.Column('ipxe_config', sa.Text(), nullable=True),
28 | sa.ForeignKeyConstraint(['node_id'], ['node.id'], ),
29 | sa.PrimaryKeyConstraint('id')
30 | )
31 | op.drop_column('node', 'active_ignition_config')
32 | # ### end Alembic commands ###
33 |
34 |
35 | def downgrade():
36 | # ### commands auto generated by Alembic - please adjust! ###
37 | op.add_column('node', sa.Column('active_ignition_config', sa.TEXT(), autoincrement=False, nullable=False))
38 | op.drop_table('provision')
39 | # ### end Alembic commands ###
40 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/35099fc974d2_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 35099fc974d2
4 | Revises: ae00e7974dca
5 | Create Date: 2017-05-19 17:01:48.878196
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '35099fc974d2'
14 | down_revision = 'ae00e7974dca'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | op.drop_column('node', 'wipe_root_disk_next_boot')
21 | op.drop_column('node', 'root_partition_size_sectors')
22 | op.drop_column('node', 'root_disk')
23 |
24 |
25 | def downgrade():
26 | op.add_column('node', sa.Column('root_disk', sa.VARCHAR(length=80), autoincrement=False, nullable=False))
27 | op.add_column('node', sa.Column('root_partition_size_sectors', sa.INTEGER(), autoincrement=False, nullable=True))
28 | op.add_column('node', sa.Column('wipe_root_disk_next_boot', sa.BOOLEAN(), autoincrement=False, nullable=False))
29 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/450edaa5f10d_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 450edaa5f10d
4 | Revises: bf3ac4dda2ab
5 | Create Date: 2017-04-30 21:33:02.865692
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '450edaa5f10d'
14 | down_revision = 'bf3ac4dda2ab'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.alter_column('cluster', 'suppose_etcd_cluster_exists', new_column_name='assert_etcd_cluster_exists')
22 | op.alter_column('cluster', 'apiservers_audit_log', new_column_name='k8s_apiservers_audit_log')
23 | op.alter_column('cluster', 'apiservers_swagger_ui', new_column_name='k8s_apiservers_swagger_ui')
24 | op.alter_column('node', 'root_disk_size_sectors', new_column_name='root_partition_size_sectors')
25 | # ### end Alembic commands ###
26 |
27 |
28 | def downgrade():
29 | # ### commands auto generated by Alembic - please adjust! ###
30 | op.alter_column('node', 'root_partition_size_sectors', new_column_name='root_disk_size_sectors')
31 | op.alter_column('cluster', 'k8s_apiservers_swagger_ui', new_column_name='apiservers_swagger_ui')
32 | op.alter_column('cluster', 'assert_etcd_cluster_exists', new_column_name='suppose_etcd_cluster_exists')
33 | op.alter_column('cluster', 'k8s_apiservers_audit_log', new_column_name='apiservers_audit_log')
34 | # ### end Alembic commands ###
35 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/469154bafe22_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 469154bafe22
4 | Revises: 450edaa5f10d
5 | Create Date: 2017-04-30 22:28:25.540557
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '469154bafe22'
14 | down_revision = '450edaa5f10d'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('cluster', sa.Column('k8s_service_account_private_key', sa.Binary(), nullable=True))
22 | op.add_column('cluster', sa.Column('k8s_service_account_public_key', sa.Binary(), nullable=True))
23 | op.execute('UPDATE cluster SET k8s_service_account_private_key=(SELECT key FROM credentials_data WHERE credentials_data.id = cluster.service_account_keypair_id)')
24 | op.execute('UPDATE cluster SET k8s_service_account_public_key=(SELECT cert FROM credentials_data WHERE credentials_data.id = cluster.service_account_keypair_id)')
25 | op.alter_column('cluster', 'k8s_service_account_private_key', nullable=False)
26 | op.alter_column('cluster', 'k8s_service_account_public_key', nullable=False)
27 | op.drop_constraint('cluster_service_account_keypair_id_fkey', 'cluster', type_='foreignkey')
28 | op.drop_column('cluster', 'service_account_keypair_id')
29 | # ### end Alembic commands ###
30 |
31 |
32 | def downgrade():
33 | # ### commands auto generated by Alembic - please adjust! ###
34 | op.add_column('cluster', sa.Column('service_account_keypair_id', sa.INTEGER(), autoincrement=False, nullable=False))
35 | op.create_foreign_key('cluster_service_account_keypair_id_fkey', 'cluster', 'credentials_data', ['service_account_keypair_id'], ['id'])
36 | op.drop_column('cluster', 'k8s_service_account_public_key')
37 | op.drop_column('cluster', 'k8s_service_account_private_key')
38 | # ### end Alembic commands ###
39 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/519c11fc090d_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 519c11fc090d
4 | Revises: 579f89f6b1a0
5 | Create Date: 2017-05-19 15:37:45.597324
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '519c11fc090d'
14 | down_revision = '579f89f6b1a0'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('disk',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('node_id', sa.Integer(), nullable=False),
24 | sa.Column('device', sa.String(length=80), nullable=False),
25 | sa.Column('wipe_next_boot', sa.Boolean(), nullable=False),
26 | sa.Column('sector_size_bytes', sa.Integer(), nullable=False),
27 | sa.ForeignKeyConstraint(['node_id'], ['node.id'], ),
28 | sa.PrimaryKeyConstraint('id'),
29 | sa.UniqueConstraint('node_id', 'device', name='_node_disk_device_uc')
30 | )
31 |
32 | op.create_table('disk_partition',
33 | sa.Column('id', sa.Integer(), nullable=False),
34 | sa.Column('disk_id', sa.Integer(), nullable=False),
35 | sa.Column('number', sa.Integer(), nullable=False),
36 | sa.Column('label', sa.String(length=80), nullable=False),
37 | sa.Column('size_mibs', sa.Integer(), nullable=True),
38 | sa.Column('format', sa.String(length=10), nullable=False),
39 | sa.ForeignKeyConstraint(['disk_id'], ['disk.id'], ),
40 | sa.PrimaryKeyConstraint('id'),
41 | sa.UniqueConstraint('disk_id', 'label', name='_disk_partition_label_uc'),
42 | sa.UniqueConstraint('disk_id', 'number', name='_disk_partition_number_uc')
43 | )
44 |
45 | op.add_column('mountpoint', sa.Column('is_persistent', sa.Boolean(), nullable=False))
46 | # ### end Alembic commands ###
47 |
48 |
49 | def downgrade():
50 | # ### commands auto generated by Alembic - please adjust! ###
51 | op.drop_column('mountpoint', 'is_persistent')
52 | op.drop_table('disk_partition')
53 | op.drop_table('disk')
54 | # ### end Alembic commands ###
55 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/52e82e7e9376_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 52e82e7e9376
4 | Revises: 635f6467bb33
5 | Create Date: 2017-05-01 08:35:20.772434
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '52e82e7e9376'
14 | down_revision = '635f6467bb33'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_unique_constraint('_cluster_name_uc', 'user', ['cluster_id', 'name'])
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_constraint('_cluster_name_uc', 'user', type_='unique')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/579f89f6b1a0_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 579f89f6b1a0
4 | Revises: 283dc9256301
5 | Create Date: 2017-05-07 04:48:43.309982
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '579f89f6b1a0'
14 | down_revision = '283dc9256301'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | op.execute('ALTER TABLE cluster ALTER COLUMN k8s_admission_control TYPE varchar(255)')
21 |
22 |
23 | def downgrade():
24 | pass
25 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/635f6467bb33_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 635f6467bb33
4 | Revises: 469154bafe22
5 | Create Date: 2017-04-30 22:39:13.776090
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '635f6467bb33'
14 | down_revision = '469154bafe22'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.alter_column('user', 'groups', new_column_name='k8s_groups')
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.alter_column('user', 'k8s_groups', new_column_name='groups')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/91248bf831b8_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 91248bf831b8
4 | Revises: b70159a8c7c2
5 | Create Date: 2017-04-27 17:31:28.639958
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '91248bf831b8'
14 | down_revision = 'b70159a8c7c2'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('cluster', sa.Column('k8s_is_rbac_enabled', sa.Boolean(), nullable=False, server_default='TRUE'))
22 | op.alter_column('cluster', 'k8s_is_rbac_enabled', server_default=None)
23 | # ### end Alembic commands ###
24 |
25 |
26 | def downgrade():
27 | # ### commands auto generated by Alembic - please adjust! ###
28 | op.drop_column('cluster', 'k8s_is_rbac_enabled')
29 | # ### end Alembic commands ###
30 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/92f518191c12_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 92f518191c12
4 | Revises: 91248bf831b8
5 | Create Date: 2017-04-27 17:44:57.672006
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 | from seedbox import config
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision = '92f518191c12'
16 | down_revision = '91248bf831b8'
17 | branch_labels = None
18 | depends_on = None
19 |
20 |
21 | def upgrade():
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | op.add_column('cluster', sa.Column('k8s_admission_control', sa.String(length=80), nullable=False,
24 | server_default=config.default_k8s_admission_control))
25 | op.alter_column('cluster', 'k8s_admission_control', server_default=None)
26 | # ### end Alembic commands ###
27 |
28 |
29 | def downgrade():
30 | # ### commands auto generated by Alembic - please adjust! ###
31 | op.drop_column('cluster', 'k8s_admission_control')
32 | # ### end Alembic commands ###
33 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/9a65898a0e2f_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 9a65898a0e2f
4 | Revises: aa55bedebb6f
5 | Create Date: 2017-04-30 12:04:42.566828
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 | from seedbox import config
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision = '9a65898a0e2f'
16 | down_revision = 'aa55bedebb6f'
17 | branch_labels = None
18 | depends_on = None
19 |
20 |
21 | def upgrade():
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | op.add_column('cluster', sa.Column('coreos_channel', sa.String(length=80), nullable=False,
24 | server_default=config.default_coreos_channel))
25 | op.alter_column('cluster', 'coreos_channel', server_default=None)
26 | op.add_column('cluster', sa.Column('coreos_version', sa.String(length=80), nullable=False,
27 | server_default=config.default_coreos_version))
28 | op.alter_column('cluster', 'coreos_version', server_default=None)
29 | op.alter_column('cluster', 'boot_images_base_url', new_column_name='custom_coreos_images_base_url')
30 | # ### end Alembic commands ###
31 |
32 |
33 | def downgrade():
34 | # ### commands auto generated by Alembic - please adjust! ###
35 | op.alter_column('cluster', 'custom_coreos_images_base_url', new_column_name='boot_images_base_url')
36 | op.drop_column('cluster', 'coreos_version')
37 | op.drop_column('cluster', 'coreos_channel')
38 | # ### end Alembic commands ###
39 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nailgun/seedbox/d124f71017dbbe5af81592e76933809b5cdddb08/src/seedbox/migrations/versions/__init__.py
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/aa55bedebb6f_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: aa55bedebb6f
4 | Revises: 2093be28531b
5 | Create Date: 2017-04-28 23:30:59.720792
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'aa55bedebb6f'
14 | down_revision = '2093be28531b'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.execute('UPDATE node SET active_config_version=0 WHERE active_config_version IS NULL')
22 | op.alter_column('node', 'active_config_version', nullable=False)
23 | # ### end Alembic commands ###
24 |
25 |
26 | def downgrade():
27 | # ### commands auto generated by Alembic - please adjust! ###
28 | op.alter_column('node', 'active_config_version', nullable=True)
29 | # ### end Alembic commands ###
30 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/ae00e7974dca_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: ae00e7974dca
4 | Revises: 519c11fc090d
5 | Create Date: 2017-05-19 17:00:48.920776
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'ae00e7974dca'
14 | down_revision = '519c11fc090d'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | from seedbox import models
21 | session = models.db.session
22 |
23 | models.Node.root_disk = sa.Column(sa.String(80), nullable=False)
24 | models.Node.wipe_root_disk_next_boot = sa.Column(sa.Boolean, nullable=False)
25 | models.Node.root_partition_size_sectors = sa.Column(sa.Integer, nullable=True)
26 |
27 | for node in models.Node.query.all():
28 | disk = models.Disk()
29 | disk.node = node
30 | disk.device = node.root_disk
31 | disk.wipe_next_boot = node.wipe_root_disk_next_boot
32 | session.add(disk)
33 |
34 | partition = models.DiskPartition()
35 | partition.disk = disk
36 | partition.number = 1
37 | partition.label = 'ROOT'
38 | partition.format = 'ext4'
39 |
40 | if node.root_partition_size_sectors:
41 | partition.size_mibs = node.root_partition_size_sectors * 512 // 1024 // 1024
42 |
43 | session.add(partition)
44 |
45 | if partition.size_mibs:
46 | partition = models.DiskPartition()
47 | partition.disk = disk
48 | partition.number = 2
49 | partition.label = 'Persistent'
50 | partition.format = 'ext4'
51 | session.add(partition)
52 |
53 | mountpoint = models.Mountpoint()
54 | mountpoint.node = node
55 | mountpoint.what = disk.device + '2'
56 | mountpoint.where = '/mnt/persistent'
57 | mountpoint.is_persistent = True
58 | session.add(mountpoint)
59 |
60 | session.commit()
61 |
62 |
63 | def downgrade():
64 | pass
65 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/b70159a8c7c2_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: b70159a8c7c2
4 | Revises: c821404e777f
5 | Create Date: 2017-04-13 00:19:37.040664
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'b70159a8c7c2'
14 | down_revision = '028018cd8818'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('node', sa.Column('debug_boot', sa.Boolean(), nullable=False, server_default='FALSE'))
22 | op.alter_column('node', 'debug_boot', server_default=None)
23 |
24 | # ### end Alembic commands ###
25 |
26 |
27 | def downgrade():
28 | # ### commands auto generated by Alembic - please adjust! ###
29 | op.drop_column('node', 'debug_boot')
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/bf3ac4dda2ab_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: bf3ac4dda2ab
4 | Revises: 9a65898a0e2f
5 | Create Date: 2017-04-30 19:28:10.308314
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'bf3ac4dda2ab'
14 | down_revision = '9a65898a0e2f'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.drop_column('cluster', 'allow_insecure_provision')
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.add_column('cluster', sa.Column('allow_insecure_provision', sa.BOOLEAN(), autoincrement=False, nullable=False))
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/da3084151a1d_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: da3084151a1d
4 | Revises: f08e29e85dc0
5 | Create Date: 2017-04-28 16:55:46.427969
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 | from seedbox import config
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision = 'da3084151a1d'
16 | down_revision = 'f08e29e85dc0'
17 | branch_labels = None
18 | depends_on = None
19 |
20 |
21 | def upgrade():
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | op.add_column('cluster', sa.Column('etcd_image_tag', sa.String(length=80), nullable=False,
24 | server_default=config.default_etcd_image_tag))
25 | op.alter_column('cluster', 'etcd_image_tag', server_default=None)
26 | # ### end Alembic commands ###
27 |
28 |
29 | def downgrade():
30 | # ### commands auto generated by Alembic - please adjust! ###
31 | op.drop_column('cluster', 'etcd_image_tag')
32 | # ### end Alembic commands ###
33 |
--------------------------------------------------------------------------------
/src/seedbox/migrations/versions/f08e29e85dc0_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: f08e29e85dc0
4 | Revises: 92f518191c12
5 | Create Date: 2017-04-28 15:48:06.677425
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'f08e29e85dc0'
14 | down_revision = '92f518191c12'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.drop_column('cluster', 'etcd_version')
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.add_column('cluster', sa.Column('etcd_version', sa.INTEGER(), autoincrement=False, nullable=False))
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/src/seedbox/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | from .address import Address
3 | from .cluster import Cluster
4 | from .credentials_data import CredentialsData
5 | from .disk import Disk
6 | from .disk_partition import DiskPartition
7 | from .mountpoint import Mountpoint
8 | from .node import Node
9 | from .provision import Provision
10 | from .user import User
11 |
--------------------------------------------------------------------------------
/src/seedbox/models/address.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Address(db.Model):
5 | id = db.Column(db.Integer, primary_key=True)
6 |
7 | node_id = db.Column(db.Integer, db.ForeignKey('node.id'), nullable=False)
8 | node = db.relationship('Node', backref=db.backref('addresses', lazy='dynamic'))
9 |
10 | interface = db.Column(db.String(80), nullable=False)
11 | ip = db.Column(db.String(80), nullable=False)
12 |
13 | def __repr__(self):
14 | return '' % self.id
15 |
16 | def __str__(self):
17 | return '{}: {}'.format(self.interface, self.ip)
18 |
--------------------------------------------------------------------------------
/src/seedbox/models/cluster.py:
--------------------------------------------------------------------------------
1 | from seedbox import pki, config, exceptions
2 | from .db import db
3 |
4 |
5 | class Cluster(db.Model):
6 | id = db.Column(db.Integer, primary_key=True)
7 | name = db.Column(db.String(80), unique=True, nullable=False)
8 |
9 | ca_credentials_id = db.Column(db.Integer, db.ForeignKey('credentials_data.id'), nullable=False)
10 | ca_credentials = db.relationship('CredentialsData', foreign_keys=[ca_credentials_id])
11 | install_dnsmasq = db.Column(db.Boolean, nullable=False, default=True)
12 |
13 | etcd_image_tag = db.Column(db.String(80), default=config.default_etcd_image_tag, nullable=False)
14 | assert_etcd_cluster_exists = db.Column(db.Boolean, nullable=False)
15 | etcd_nodes_dns_name = db.Column(db.String(80), default='', nullable=False)
16 |
17 | k8s_apiservers_audit_log = db.Column(db.Boolean, nullable=False)
18 | k8s_apiservers_swagger_ui = db.Column(db.Boolean, nullable=False)
19 | dnsmasq_static_records = db.Column(db.Boolean, nullable=False)
20 |
21 | # workaround for a VirtualBox environment issue
22 | # https://github.com/coreos/flannel/issues/98
23 | explicitly_advertise_addresses = db.Column(db.Boolean, nullable=False)
24 |
25 | k8s_pod_network = db.Column(db.String(80), default=config.default_k8s_pod_network, nullable=False)
26 | k8s_service_network = db.Column(db.String(80), default=config.default_k8s_service_network, nullable=False)
27 | k8s_hyperkube_tag = db.Column(db.String(80), default=config.default_k8s_hyperkube_tag, nullable=False)
28 | k8s_cni = db.Column(db.Boolean, nullable=False)
29 | k8s_apiservers_dns_name = db.Column(db.String(80), default='', nullable=False)
30 | k8s_is_rbac_enabled = db.Column(db.Boolean, nullable=False, default=True)
31 | k8s_admission_control = db.Column(db.String(255), default=config.default_k8s_admission_control, nullable=False)
32 |
33 | coreos_channel = db.Column(db.String(80), default=config.default_coreos_channel, nullable=False)
34 | coreos_version = db.Column(db.String(80), default=config.default_coreos_version, nullable=False)
35 | custom_coreos_images_base_url = db.Column(db.String(80), default='', nullable=False)
36 |
37 | aci_proxy_url = db.Column(db.String(80), default='', nullable=False)
38 | aci_proxy_ca_cert = db.Column(db.Text, default='', nullable=False)
39 |
40 | k8s_service_account_public_key = db.Column(db.Binary, nullable=False)
41 | k8s_service_account_private_key = db.Column(db.Binary, nullable=False)
42 |
43 | docker_config = db.Column(db.Text, default='{}', nullable=False)
44 |
45 | def __repr__(self):
46 | return '' % self.name
47 |
48 | def __str__(self):
49 | return self.name
50 |
51 | @property
52 | def ca_credentials_error(self):
53 | try:
54 | pki.validate_certificate_common_name(self.ca_credentials.cert, self.name)
55 | pki.validate_ca_certificate_constraints(self.ca_credentials.cert)
56 | except pki.InvalidCertificate as e:
57 | return str(e)
58 |
59 | @property
60 | def k8s_apiserver_service_ip(self):
61 | ip = self.k8s_service_network.split('/')[0]
62 | return ip.rsplit('.', maxsplit=1)[0] + '.1'
63 |
64 | @property
65 | def k8s_dns_service_ip(self):
66 | ip = self.k8s_service_network.split('/')[0]
67 | return ip.rsplit('.', maxsplit=1)[0] + '.10'
68 |
69 | @property
70 | def k8s_apiserver_nodes(self):
71 | return self.nodes.filter_by(is_k8s_master=True)
72 |
73 | @property
74 | def k8s_apiserver_endpoint(self):
75 | if self.k8s_apiservers_dns_name:
76 | host = self.k8s_apiservers_dns_name
77 | else:
78 | apiserver = self.k8s_apiserver_nodes.first()
79 | if apiserver is None:
80 | raise exceptions.K8sNoClusterApiserver()
81 | host = apiserver.fqdn
82 | return 'https://{}:{}'.format(host, config.k8s_apiserver_secure_port)
83 |
84 | @property
85 | def etcd_nodes(self):
86 | return self.nodes.filter_by(is_etcd_server=True)
87 |
88 | @property
89 | def etcd_client_endpoints(self):
90 | if self.etcd_nodes_dns_name:
91 | hosts = [self.etcd_nodes_dns_name]
92 | else:
93 | hosts = [n.fqdn for n in self.etcd_nodes]
94 | return ['https://{}:{}'.format(host, config.etcd_client_port) for host in hosts]
95 |
96 | @property
97 | def is_configured(self):
98 | from .node import Node
99 | return not self.nodes.filter(Node.target_config_version != Node.active_config_version).count()
100 |
101 | @property
102 | def are_etcd_nodes_configured(self):
103 | from .node import Node
104 | return not self.nodes.filter(Node.is_etcd_server,
105 | Node.target_config_version != Node.active_config_version).count()
106 |
107 | @property
108 | def coreos_images_base_url(self):
109 | if self.custom_coreos_images_base_url:
110 | return self.custom_coreos_images_base_url
111 | else:
112 | return config.default_coreos_images_base_url.format(channel=self.coreos_channel,
113 | version=self.coreos_version)
114 |
115 | @property
116 | def k8s_kube_proxy_user(self):
117 | return self.users.filter_by(name=config.k8s_kube_proxy_user_name).first()
118 |
--------------------------------------------------------------------------------
/src/seedbox/models/credentials_data.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class CredentialsData(db.Model):
5 | id = db.Column(db.Integer, primary_key=True)
6 | cert = db.Column(db.Binary, nullable=False)
7 | key = db.Column(db.Binary, nullable=False)
8 |
9 | def __repr__(self):
10 | return '' % self.id
11 |
--------------------------------------------------------------------------------
/src/seedbox/models/db.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | db = SQLAlchemy()
3 |
--------------------------------------------------------------------------------
/src/seedbox/models/disk.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.schema import UniqueConstraint
2 |
3 | from .db import db
4 |
5 |
6 | class Disk(db.Model):
7 | id = db.Column(db.Integer, primary_key=True)
8 |
9 | node_id = db.Column(db.Integer, db.ForeignKey('node.id'), nullable=False)
10 | node = db.relationship('Node', backref=db.backref('disks', lazy='dynamic'))
11 |
12 | device = db.Column(db.String(80), nullable=False)
13 | wipe_next_boot = db.Column(db.Boolean, nullable=False, default=True)
14 | sector_size_bytes = db.Column(db.Integer, nullable=False, default=512)
15 |
16 | __table_args__ = (
17 | UniqueConstraint('node_id', 'device', name='_node_disk_device_uc'),
18 | )
19 |
20 | def __repr__(self):
21 | return '' % self.id
22 |
23 | def __str__(self):
24 | return self.device
25 |
--------------------------------------------------------------------------------
/src/seedbox/models/disk_partition.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.schema import UniqueConstraint
2 |
3 | from .db import db
4 |
5 |
6 | class DiskPartition(db.Model):
7 | id = db.Column(db.Integer, primary_key=True)
8 |
9 | disk_id = db.Column(db.Integer, db.ForeignKey('disk.id'), nullable=False)
10 | disk = db.relationship('Disk', backref=db.backref('partitions', lazy='dynamic'))
11 |
12 | number = db.Column(db.Integer, nullable=False)
13 | label = db.Column(db.String(80), nullable=False)
14 | size_mibs = db.Column(db.Integer, nullable=True)
15 | format = db.Column(db.String(10), nullable=False, default='ext4')
16 |
17 | __table_args__ = (
18 | UniqueConstraint('disk_id', 'number', name='_disk_partition_number_uc'),
19 | UniqueConstraint('disk_id', 'label', name='_disk_partition_label_uc'),
20 | )
21 |
22 | def __repr__(self):
23 | return '' % self.id
24 |
25 | def __str__(self):
26 | return self.label
27 |
28 | @property
29 | def is_root(self):
30 | return self.label == 'ROOT'
31 |
32 | @property
33 | def device(self):
34 | return '{}{}'.format(self.disk.device, self.number)
35 |
--------------------------------------------------------------------------------
/src/seedbox/models/mountpoint.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.schema import UniqueConstraint
2 |
3 | from .db import db
4 |
5 |
6 | class Mountpoint(db.Model):
7 | id = db.Column(db.Integer, primary_key=True)
8 |
9 | node_id = db.Column(db.Integer, db.ForeignKey('node.id'), nullable=False)
10 | node = db.relationship('Node', backref=db.backref('mountpoints', lazy='dynamic'))
11 |
12 | what = db.Column(db.String(80), nullable=False)
13 | where = db.Column(db.String(80), nullable=False)
14 | wanted_by = db.Column(db.String(80), default='local-fs.target', nullable=False)
15 | is_persistent = db.Column(db.Boolean, nullable=False)
16 |
17 | __table_args__ = (
18 | UniqueConstraint('node_id', 'what', name='_node_mountpoint_what_uc'),
19 | UniqueConstraint('node_id', 'where', name='_node_mountpoint_where_uc'),
20 | )
21 |
22 | def __repr__(self):
23 | return '' % self.id
24 |
25 | def __str__(self):
26 | return '{} -> {}'.format(self.what, self.where)
27 |
--------------------------------------------------------------------------------
/src/seedbox/models/node.py:
--------------------------------------------------------------------------------
1 | from seedbox import pki, config, exceptions
2 | from .db import db
3 |
4 |
5 | class Node(db.Model):
6 | id = db.Column(db.Integer, primary_key=True)
7 | ip = db.Column(db.String(80), unique=True, nullable=False)
8 | fqdn = db.Column(db.String(80), unique=True, nullable=False)
9 |
10 | maintenance_mode = db.Column(db.Boolean, nullable=False)
11 | debug_boot = db.Column(db.Boolean, nullable=False)
12 | additional_kernel_cmdline = db.Column(db.String(255), default='', nullable=False)
13 |
14 | cluster_id = db.Column(db.Integer, db.ForeignKey('cluster.id'), nullable=False)
15 | cluster = db.relationship('Cluster', backref=db.backref('nodes', lazy='dynamic'))
16 |
17 | credentials_id = db.Column(db.Integer, db.ForeignKey('credentials_data.id'), nullable=False)
18 | credentials = db.relationship('CredentialsData')
19 |
20 | target_config_version = db.Column(db.Integer, default=1, nullable=False)
21 | active_config_version = db.Column(db.Integer, default=0, nullable=False)
22 |
23 | coreos_autologin = db.Column(db.Boolean, nullable=False)
24 | linux_consoles = db.Column(db.String(80), default=config.default_linux_consoles, nullable=False)
25 | disable_ipv6 = db.Column(db.Boolean, nullable=False)
26 |
27 | is_etcd_server = db.Column(db.Boolean, nullable=False)
28 | is_k8s_schedulable = db.Column(db.Boolean, default=True, nullable=False)
29 | is_k8s_master = db.Column(db.Boolean, nullable=False)
30 |
31 | def __repr__(self):
32 | return '' % self.fqdn
33 |
34 | def __str__(self):
35 | return self.fqdn
36 |
37 | @property
38 | def is_config_match(self):
39 | return self.target_config_version == self.active_config_version
40 |
41 | @property
42 | def certificate_alternative_dns_names(self):
43 | names = [self.fqdn]
44 |
45 | if self.is_etcd_server and self.cluster.etcd_nodes_dns_name:
46 | names += [
47 | self.cluster.etcd_nodes_dns_name,
48 | ]
49 |
50 | if self.is_k8s_master:
51 | if self.cluster.k8s_apiservers_dns_name:
52 | names += [
53 | self.cluster.k8s_apiservers_dns_name,
54 | ]
55 |
56 | names += [
57 | 'kubernetes',
58 | 'kubernetes.default',
59 | 'kubernetes.default.svc',
60 | 'kubernetes.default.svc.' + config.k8s_cluster_domain,
61 | ]
62 |
63 | return names
64 |
65 | @property
66 | def certificate_alternative_ips(self):
67 | ips = [self.ip]
68 |
69 | if self.is_k8s_master:
70 | ips += [self.cluster.k8s_apiserver_service_ip]
71 |
72 | return ips
73 |
74 | @property
75 | def credentials_error(self):
76 | try:
77 | pki.verify_certificate_chain(self.cluster.ca_credentials.cert, self.credentials.cert)
78 | pki.validate_certificate_common_name(self.credentials.cert, 'system:node:' + self.fqdn)
79 | pki.validate_certificate_hosts(self.credentials.cert, self.certificate_alternative_dns_names)
80 | pki.validate_certificate_organizations(self.credentials.cert, ['system:nodes'])
81 | pki.validate_certificate_key_usage(self.credentials.cert, is_web_server=True, is_web_client=True)
82 | except pki.InvalidCertificate as e:
83 | return str(e)
84 |
85 | @property
86 | def credentials_warning(self):
87 | try:
88 | pki.validate_certificate_host_ips(self.credentials.cert, self.certificate_alternative_ips)
89 | except pki.InvalidCertificate as e:
90 | return str(e)
91 |
92 | @property
93 | def root_partition(self):
94 | from .disk_partition import DiskPartition
95 | partitions = list(DiskPartition.query.filter(DiskPartition.disk.has(node_id=self.id),
96 | DiskPartition.label == 'ROOT'))
97 | if not partitions:
98 | raise exceptions.NoRootPartition()
99 | if len(partitions) > 1:
100 | raise exceptions.MultipleRootPartitions()
101 | return partitions[0]
102 |
103 | @property
104 | def persistent_mountpoint(self):
105 | mountpoints = list(self.mountpoints.filter_by(is_persistent=True))
106 | if not mountpoints:
107 | return None
108 | if len(mountpoints) > 1:
109 | raise exceptions.MultiplePersistentMountpoints()
110 | return mountpoints[0]
111 |
112 | @property
113 | def active_config(self):
114 | from . import Provision
115 | if not self.active_config_version:
116 | return None
117 | else:
118 | return self.provisions.order_by(Provision.applied_at.desc()).first()
119 |
120 | @property
121 | def disks_error(self):
122 | try:
123 | _ = self.root_partition
124 | except (exceptions.NoRootPartition, exceptions.MultipleRootPartitions) as e:
125 | return str(e)
126 | try:
127 | _ = self.persistent_mountpoint
128 | except exceptions.MultiplePersistentMountpoints as e:
129 | return str(e)
130 |
131 | @property
132 | def disks_warning(self):
133 | if not self.persistent_mountpoint:
134 | return 'No persistent mountpoint defined'
135 |
--------------------------------------------------------------------------------
/src/seedbox/models/provision.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from .db import db
4 |
5 |
6 | class Provision(db.Model):
7 | id = db.Column(db.Integer, primary_key=True)
8 |
9 | node_id = db.Column(db.Integer, db.ForeignKey('node.id'), nullable=False)
10 | node = db.relationship('Node', backref=db.backref('provisions', lazy='dynamic'))
11 | applied_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
12 |
13 | config_version = db.Column(db.Integer, nullable=False)
14 | ignition_config = db.Column(db.Binary, nullable=True)
15 | ipxe_config = db.Column(db.Text, nullable=True)
16 |
--------------------------------------------------------------------------------
/src/seedbox/models/user.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.schema import UniqueConstraint
2 |
3 | from seedbox import pki
4 | from .db import db
5 |
6 |
7 | class User(db.Model):
8 | id = db.Column(db.Integer, primary_key=True)
9 | cluster_id = db.Column(db.Integer, db.ForeignKey('cluster.id'), nullable=False)
10 | cluster = db.relationship('Cluster', backref=db.backref('users', lazy='dynamic'))
11 | name = db.Column(db.String(80), nullable=False)
12 | credentials_id = db.Column(db.Integer, db.ForeignKey('credentials_data.id'), nullable=False)
13 | credentials = db.relationship('CredentialsData')
14 | k8s_groups = db.Column(db.String(255), nullable=False, default='')
15 | ssh_key = db.Column(db.Text, nullable=False, default='')
16 |
17 | __table_args__ = (
18 | UniqueConstraint('cluster_id', 'name', name='_cluster_name_uc'),
19 | )
20 |
21 | def __repr__(self):
22 | return '' % self.name
23 |
24 | def __str__(self):
25 | return self.name
26 |
27 | @property
28 | def credentials_error(self):
29 | try:
30 | pki.verify_certificate_chain(self.cluster.ca_credentials.cert, self.credentials.cert)
31 | pki.validate_certificate_common_name(self.credentials.cert, self.name)
32 | if self.k8s_groups:
33 | pki.validate_certificate_organizations(self.credentials.cert, self.k8s_groups.split(','))
34 | pki.validate_certificate_key_usage(self.credentials.cert, is_web_server=False, is_web_client=True)
35 | except pki.InvalidCertificate as e:
36 | return str(e)
37 |
--------------------------------------------------------------------------------
/src/seedbox/pki.py:
--------------------------------------------------------------------------------
1 | import ssl
2 | import idna
3 | import datetime
4 | import operator
5 | import ipaddress
6 | import subprocess
7 | from OpenSSL import crypto
8 | from cryptography import x509
9 | from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID
10 | from cryptography.hazmat.backends import default_backend
11 | from cryptography.hazmat.primitives import hashes
12 | from cryptography.hazmat.primitives import serialization
13 | from cryptography.hazmat.primitives.asymmetric import rsa
14 |
15 |
16 | def generate_rsa_keypair(key_size=2048):
17 | key = rsa.generate_private_key(public_exponent=65537, key_size=key_size, backend=default_backend())
18 | public_key = key.public_key()
19 | public = public_key.public_bytes(encoding=serialization.Encoding.PEM,
20 | format=serialization.PublicFormat.SubjectPublicKeyInfo)
21 | private = key.private_bytes(encoding=serialization.Encoding.PEM,
22 | format=serialization.PrivateFormat.TraditionalOpenSSL,
23 | encryption_algorithm=serialization.NoEncryption())
24 | return public, private
25 |
26 |
27 | def create_ca_certificate(cn, key_size=4096, certify_days=365):
28 | key = rsa.generate_private_key(public_exponent=65537, key_size=key_size, backend=default_backend())
29 | key_id = x509.SubjectKeyIdentifier.from_public_key(key.public_key())
30 |
31 | subject = issuer = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)])
32 |
33 | now = datetime.datetime.utcnow()
34 | serial = x509.random_serial_number()
35 | cert = x509.CertificateBuilder() \
36 | .subject_name(subject) \
37 | .issuer_name(issuer) \
38 | .public_key(key.public_key()) \
39 | .serial_number(serial) \
40 | .not_valid_before(now) \
41 | .not_valid_after(now + datetime.timedelta(days=certify_days)) \
42 | .add_extension(key_id, critical=False) \
43 | .add_extension(x509.AuthorityKeyIdentifier(key_id.digest,
44 | [x509.DirectoryName(issuer)],
45 | serial),
46 | critical=False) \
47 | .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True) \
48 | .add_extension(x509.KeyUsage(digital_signature=True,
49 | content_commitment=False,
50 | key_encipherment=False,
51 | data_encipherment=False,
52 | key_agreement=False,
53 | key_cert_sign=True,
54 | crl_sign=True,
55 | encipher_only=False,
56 | decipher_only=False),
57 | critical=True) \
58 | .sign(key, hashes.SHA256(), default_backend())
59 |
60 | cert = cert.public_bytes(serialization.Encoding.PEM)
61 | key = key.private_bytes(encoding=serialization.Encoding.PEM,
62 | format=serialization.PrivateFormat.TraditionalOpenSSL,
63 | encryption_algorithm=serialization.NoEncryption())
64 | return cert, key
65 |
66 |
67 | def issue_certificate(cn, ca_cert, ca_key,
68 | organizations=(),
69 | san_dns=(),
70 | san_ips=(),
71 | key_size=2048,
72 | certify_days=365,
73 | is_web_server=False,
74 | is_web_client=False):
75 | ca_cert = x509.load_pem_x509_certificate(ca_cert, default_backend())
76 | ca_key = serialization.load_pem_private_key(ca_key, password=None, backend=default_backend())
77 | ca_key_id = x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key())
78 |
79 | key = rsa.generate_private_key(public_exponent=65537, key_size=key_size, backend=default_backend())
80 |
81 | subject_name_attributes = [x509.NameAttribute(NameOID.COMMON_NAME, cn)]
82 | subject_name_attributes += [x509.NameAttribute(NameOID.ORGANIZATION_NAME, org) for org in organizations]
83 | subject = x509.Name(subject_name_attributes)
84 |
85 | now = datetime.datetime.utcnow()
86 | cert = x509.CertificateBuilder() \
87 | .subject_name(subject) \
88 | .issuer_name(ca_cert.issuer) \
89 | .public_key(key.public_key()) \
90 | .serial_number(x509.random_serial_number()) \
91 | .not_valid_before(now) \
92 | .not_valid_after(now + datetime.timedelta(days=certify_days)) \
93 | .add_extension(x509.AuthorityKeyIdentifier(ca_key_id.digest,
94 | [x509.DirectoryName(ca_cert.issuer)],
95 | ca_cert.serial_number),
96 | critical=False) \
97 | .add_extension(x509.KeyUsage(digital_signature=True,
98 | content_commitment=False,
99 | key_encipherment=True,
100 | data_encipherment=False,
101 | key_agreement=False,
102 | key_cert_sign=False,
103 | crl_sign=False,
104 | encipher_only=False,
105 | decipher_only=False),
106 | critical=True)
107 |
108 | extended_usages = []
109 | if is_web_server:
110 | extended_usages.append(ExtendedKeyUsageOID.SERVER_AUTH)
111 | if is_web_client:
112 | extended_usages.append(ExtendedKeyUsageOID.CLIENT_AUTH)
113 | if extended_usages:
114 | cert = cert.add_extension(x509.ExtendedKeyUsage(extended_usages), critical=False)
115 |
116 | sans = [x509.DNSName(name) for name in san_dns]
117 | sans += [x509.IPAddress(ipaddress.ip_address(ip)) for ip in san_ips]
118 | if sans:
119 | cert = cert.add_extension(x509.SubjectAlternativeName(sans), critical=False)
120 |
121 | cert = cert.sign(ca_key, hashes.SHA256(), default_backend())
122 |
123 | cert = cert.public_bytes(serialization.Encoding.PEM)
124 | key = key.private_bytes(encoding=serialization.Encoding.PEM,
125 | format=serialization.PrivateFormat.TraditionalOpenSSL,
126 | encryption_algorithm=serialization.NoEncryption())
127 | return cert, key
128 |
129 |
130 | def get_certificate_text(cert):
131 | return subprocess.run(['openssl', 'x509',
132 | '-in', '/dev/stdin',
133 | '-text'],
134 | check=True,
135 | stdout=subprocess.PIPE,
136 | input=cert).stdout
137 |
138 |
139 | def verify_certificate_chain(ca_pem_data, cert_pem_data):
140 | try:
141 | ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_pem_data)
142 | cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem_data)
143 |
144 | store = crypto.X509Store()
145 | store.add_cert(ca_cert)
146 |
147 | store_ctx = crypto.X509StoreContext(store, cert)
148 | store_ctx.verify_certificate()
149 | except crypto.Error as e:
150 | raise InvalidCertificate('Broken certificate') from e
151 | except crypto.X509StoreContextError as e:
152 | raise InvalidCertificate('Invalid certificate chain: ' + str(e)) from e
153 |
154 |
155 | def wrap_subject_matching_errors(func):
156 | def wrapped(*args, **kwargs):
157 | try:
158 | return func(*args, **kwargs)
159 | except idna.core.IDNAError as e:
160 | raise InvalidCertificate('Invalid subject IDNA name') from e
161 | return wrapped
162 |
163 |
164 | @wrap_subject_matching_errors
165 | def validate_certificate_common_name(cert_pem_data, subject_name):
166 | cert = x509.load_pem_x509_certificate(cert_pem_data, default_backend())
167 | _match_subject_name(cert, subject_name, alt_names=False)
168 |
169 |
170 | @wrap_subject_matching_errors
171 | def validate_certificate_hosts(cert_pem_data, host_names):
172 | cert = x509.load_pem_x509_certificate(cert_pem_data, default_backend())
173 | for host_name in host_names:
174 | _match_subject_name(cert, host_name, compare_func=ssl._dnsname_match)
175 |
176 |
177 | @wrap_subject_matching_errors
178 | def validate_certificate_host_ips(cert_pem_data, host_ips):
179 | cert = x509.load_pem_x509_certificate(cert_pem_data, default_backend())
180 | for host_ip in host_ips:
181 | _match_subject_ip(cert, host_ip)
182 |
183 |
184 | @wrap_subject_matching_errors
185 | def validate_certificate_key_usage(cert_pem_data, is_web_server, is_web_client):
186 | cert = x509.load_pem_x509_certificate(cert_pem_data, default_backend())
187 | try:
188 | key_usage = cert.extensions.get_extension_for_oid(ExtensionOID.KEY_USAGE)
189 | key_usage = key_usage.value
190 | except x509.extensions.ExtensionNotFound:
191 | raise InvalidCertificate("Key usage not specified")
192 |
193 | if not key_usage.digital_signature:
194 | raise InvalidCertificate("Not intented for Digital Signature")
195 |
196 | if not key_usage.key_encipherment:
197 | raise InvalidCertificate("Not intented for Key Encipherment")
198 |
199 | if is_web_server or is_web_client:
200 | try:
201 | exteneded_key_usage = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE)
202 | exteneded_key_usage = exteneded_key_usage.value
203 | except x509.extensions.ExtensionNotFound:
204 | raise InvalidCertificate("Extended key usage not specified")
205 |
206 | if is_web_server:
207 | if ExtendedKeyUsageOID.SERVER_AUTH not in exteneded_key_usage:
208 | raise InvalidCertificate("Not intented for TLS Web Server Authentication")
209 |
210 | if is_web_client:
211 | if ExtendedKeyUsageOID.CLIENT_AUTH not in exteneded_key_usage:
212 | raise InvalidCertificate("Not intented for TLS Web Client Authentication")
213 |
214 |
215 | @wrap_subject_matching_errors
216 | def validate_certificate_organizations(cert_pem_data, organizations):
217 | cert = x509.load_pem_x509_certificate(cert_pem_data, default_backend())
218 | organization_names = cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
219 |
220 | organizations_required = organizations
221 | organizations_present = set(o.value for o in organization_names)
222 |
223 | for org in organizations_required:
224 | if org not in organizations_present:
225 | raise InvalidCertificate("Not member of organization {}".format(org))
226 |
227 |
228 | @wrap_subject_matching_errors
229 | def validate_ca_certificate_constraints(cert_pem_data):
230 | cert = x509.load_pem_x509_certificate(cert_pem_data, default_backend())
231 | try:
232 | constraints = cert.extensions.get_extension_for_oid(ExtensionOID.BASIC_CONSTRAINTS)
233 | constraints = constraints.value
234 | except x509.extensions.ExtensionNotFound:
235 | return
236 |
237 | if not constraints.ca:
238 | raise InvalidCertificate("Not a CA certificate")
239 |
240 | if constraints.path_length != 0:
241 | raise InvalidCertificate("Invalid pathlen")
242 |
243 |
244 | # based on ssl.match_hostname code
245 | # https://github.com/python/cpython/blob/6f0eb93183519024cb360162bdd81b9faec97ba6/Lib/ssl.py#L279
246 | def _match_subject_name(cert, subject_name, compare_func=operator.eq, alt_names=True):
247 | names = []
248 |
249 | if alt_names:
250 | try:
251 | alt_names = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
252 | names = alt_names.value.get_values_for_type(x509.DNSName)
253 | except x509.extensions.ExtensionNotFound:
254 | pass
255 |
256 | if not names:
257 | common_names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
258 | if common_names:
259 | common_name = common_names[0]
260 | names = [common_name.value]
261 |
262 | if not any(compare_func(name, subject_name) for name in names):
263 | if len(names) > 1:
264 | raise InvalidCertificate("Subject name %r doesn't match either of %s" % (subject_name, ', '.join(map(repr, names))))
265 | elif len(names) == 1:
266 | raise InvalidCertificate("Subject name %r doesn't match %r" % (subject_name, names[0]))
267 | else:
268 | raise InvalidCertificate("No appropriate commonName or subjectAltName DNSName fields were found")
269 |
270 |
271 | def _match_subject_ip(cert, subject_ip, compare_func=operator.eq):
272 | alt_names = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
273 | ips = alt_names.value.get_values_for_type(x509.IPAddress)
274 |
275 | subject_ip = ipaddress.ip_address(subject_ip)
276 | if not any(compare_func(ip, subject_ip) for ip in ips):
277 | if len(ips) > 1:
278 | raise InvalidCertificate("Subject ip %s doesn't match either of %s" % (subject_ip, ', '.join(map(repr, ips))))
279 | elif len(ips) == 1:
280 | raise InvalidCertificate("Subject ip %s doesn't match %s" % (subject_ip, ips[0]))
281 | else:
282 | raise InvalidCertificate("No appropriate subjectAltName IPAddress fields were found")
283 |
284 |
285 | class InvalidCertificate(Exception):
286 | pass
287 |
--------------------------------------------------------------------------------
/src/seedbox/update_watcher.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 | import json
4 | import logging
5 | import configparser
6 |
7 | import requests
8 |
9 | from seedbox import config
10 |
11 | log = logging.getLogger(__name__)
12 |
13 | coreos_channels = ('stable', 'beta', 'alpha')
14 | coreos_version_url = 'https://{channel}.release.core-os.net/amd64-usr/current/version.txt'
15 | quay_io_tags_url = 'https://quay.io/api/v1/repository/{repo_path}/tag/'
16 | unstable_tag_regexps = [re.compile(r'[^a-zA-Z]' + w + r'[^a-zA-Z]') for w in ('rc', 'beta', 'alpha')]
17 |
18 |
19 | def watch():
20 | while True:
21 | data = fetch()
22 | log.info('Current versions: %s', data)
23 | with open(config.update_state_file, 'w') as fp:
24 | json.dump(data, fp)
25 | log.info('Saved to %s', config.update_state_file)
26 | log.info('Waiting for %s seconds before next update', config.update_check_interval_sec)
27 | time.sleep(config.update_check_interval_sec)
28 |
29 |
30 | def fetch():
31 | versions = {}
32 |
33 | coreos_versions = {}
34 | for coreos_channel in coreos_channels:
35 | try:
36 | coreos_versions[coreos_channel] = fetch_coreos(coreos_channel)
37 | except Exception:
38 | log.exception('Failed to fetch latest CoreOS version on %s channel', coreos_channel)
39 | versions['coreos'] = coreos_versions
40 |
41 | try:
42 | versions['etcd'] = fetch_from_quay(config.etcd_image)
43 | except Exception:
44 | log.exception('Failed to fetch latest etcd version')
45 |
46 | try:
47 | versions['hyperkube'] = fetch_from_quay(config.k8s_hyperkube_image)
48 | except Exception:
49 | log.exception('Failed to fetch latest hyperkube version')
50 |
51 | return versions
52 |
53 |
54 | def fetch_coreos(channel):
55 | resp = requests.get(coreos_version_url.format(channel=channel))
56 | resp.raise_for_status()
57 | config = configparser.ConfigParser()
58 | config.read_string('[a]\n' + resp.content.decode('utf-8'))
59 | return config['a']['COREOS_VERSION']
60 |
61 |
62 | def fetch_from_quay(image_url):
63 | quay_io_image_prefix = 'quay.io/'
64 |
65 | if not image_url.startswith(quay_io_image_prefix):
66 | raise Exception('Unsupported image URL', image_url)
67 |
68 | repo_path = image_url[len(quay_io_image_prefix):]
69 | resp = requests.get(quay_io_tags_url.format(repo_path=repo_path))
70 | resp.raise_for_status()
71 |
72 | tags = [(tag['name'], tag2version(tag['name'])) for tag in resp.json()['tags'] if is_stable_tag(tag['name'])]
73 | latest = tags[0]
74 |
75 | for tag in tags:
76 | if tag[1] > latest[1]:
77 | latest = tag
78 |
79 | return latest[0]
80 |
81 |
82 | def tag2version(tag):
83 | from packaging import version
84 | return version.parse(tag)
85 |
86 |
87 | def is_stable_tag(tag):
88 | for r in unstable_tag_regexps:
89 | if r.search(tag):
90 | return False
91 | return True
92 |
--------------------------------------------------------------------------------
/src/seedbox/utils.py:
--------------------------------------------------------------------------------
1 | import io
2 | import tarfile
3 |
4 |
5 | class TarFile(tarfile.TarFile):
6 | def adddata(self, path, data):
7 | info = tarfile.TarInfo(path)
8 | info.size = len(data)
9 | self.addfile(info, io.BytesIO(data))
10 |
--------------------------------------------------------------------------------