Merge branch 'master' of github.com:robot-tools/iconograph

This commit is contained in:
Ian Gulliver
2016-04-25 11:52:39 -07:00
17 changed files with 623 additions and 77 deletions

View File

@@ -84,6 +84,55 @@ flags to build_image.py as long as the modules are compatible with each other.
Stock modules: Stock modules:
### autoimage.py
Build an image that will partition, mkfs, and install an image from a different
URL onto a target system. Used to create install USB drives, PXE boot, etc.
Use the build_image.py flag:
```bash
--module="server/modules/autoimage.py --base-url=http://yourhost/ --ca-cert=/path/to/signing/cert.pem --device=/dev/sdx --persistent-percent=50"
```
`--device` specifies the device to partition and install to on the target
system.
Optional flags:
`--persistent-percent`, if non-zero, specifies the percent of the target
device to allocate to a LABEL=PERSISTENT filesystem. If the inner image uses
persistent.py, this filesystem will be automatically mounted.
`--https-ca-cert` specifies a local path to a PEM-encoded certificate to
validate the HTTPS image server cert against. This differs from `--ca-cert`,
which is used to validate the manifest.json signature.
`--https-client-cert` and `--https-client-key` are used together to specify
local paths to a PEM-encoded certificate and key pair that will be provided
to the server over HTTPS. This can be used to limit image availability.
### certclient.py
Use a local master key/cert pair to authenticate to a
[certserver](https://github.com/robot-tools/certserver) instance and retrieve
a system-specific key. Mainly intended to be used with autoimage.py and
systemid.py.
Use the build_image.py flag:
```bash
--module="server/modules/certclient.py --server=https://certserver/ --ca-cert=/path/to/server/cert.pem --client-cert=/path/to/client/cert.pem --client-key=/path/to/client/key.pem --tag=www --subject='/C=US/ST=California/O=XXXX/OU=XXXX Test/CN=SYSTEMID'"
```
The new key and cert are saved to /systemid
`--tag` specifies a value added to the filename, so certclient.py can be
used more than once with different servers (e.g. once for an HTTPS client
key/cert pair, and once for an EAP-TLS key/cert pair).
`--subject` specifics the subject string passed to openssl. `SYSTEMID` is
replaced with the system hostname, possibly as set by systemid.py
### iconograph.py ### iconograph.py
Install icon inside the image. This allows the image to auto-update over HTTP. Install icon inside the image. This allows the image to auto-update over HTTP.
@@ -95,6 +144,10 @@ Use the build_image.py flag:
Optional flags: Optional flags:
`--https-ca-cert` specifies a local path to a PEM-encoded certificate to
validate the HTTPS image server cert against. This differs from `--ca-cert`,
which is used to validate the manifest.json signature.
`--max-images` sets the number of recent images to keep. Older images are `--max-images` sets the number of recent images to keep. Older images are
deleted. Defaults to 5. 0 means unlimited. deleted. Defaults to 5. 0 means unlimited.
@@ -108,23 +161,24 @@ Use the build_image.py flag:
--module="server/modules/persistent.py" --module="server/modules/persistent.py"
``` ```
### autoimage.py See [imager/image.py](imager/image.py)'s or
[server/module/autoimage.py](autoimage.py)'s `--persistent-percent` flag to
create this partition.
Build an image that will partition, mkfs, and install an image from a different ### systemid.py
URL onto a target system. Used to create install USB drives, PXE boot, etc.
Use the build_image.py flag: Mount a /systemid partition from a filesystem with LABEL=SYSTEMID. This is
intended to a be separate device (possibly a USB flash drive, SD card, etc.)
which contains data that persists across re-images and identifies the system,
including system-specific keys and certificates.
It also sets the hostname to the value found in the systemid config on the
device.
```bash ```bash
--module="server/modules/autoimage.py --base-url=http://yourhost/ --ca-cert=/path/to/signing/cert.pem --device=/dev/sdx --persistent-percent=50" --module="server/modules/systemid.py"
``` ```
`--device` specifies the device to partition and install to on the target
system.
`--persistent-percent`, if non-zero, specifies the percent of the target
device to allocate to a LABEL=PERSISTENT filesystem. If the inner image uses
persistent.py, this filesystem will be automatically mounted.
## Module API ## Module API
Modules are passed the following long-style arguments: Modules are passed the following long-style arguments:
@@ -184,3 +238,13 @@ or manually write them to a drive. To do so:
# Needs sudo to partition and mkfs devices # Needs sudo to partition and mkfs devices
sudo imager/image.py --base-url=http://yourhost/ --ca-cert=/path/to/signing/cert.pem --device=/dev/sdx --persistent-percent=50 sudo imager/image.py --base-url=http://yourhost/ --ca-cert=/path/to/signing/cert.pem --device=/dev/sdx --persistent-percent=50
``` ```
Optional flags:
`--https-ca-cert` specifies a local path to a PEM-encoded certificate to
validate the HTTPS image server cert against. This differs from `--ca-cert`,
which is used to validate the manifest.json signature.
`--https-client-cert` and `--https-client-key` are used together to specify
local paths to a PEM-encoded certificate and key pair that will be provided
to the server over HTTPS. This can be used to limit image availability.

1
client/.gitignore vendored
View File

@@ -1 +0,0 @@
flags

View File

@@ -1,15 +1,30 @@
#!/bin/sh #!/bin/bash
set -ex set -ex
BASE=$(dirname $0) BASE=$(dirname $0)
IMAGES=/isodevice/iconograph IMAGES="/isodevice/iconograph"
mkdir -p "${IMAGES}" mkdir -p "${IMAGES}"
BOOT=/isodevice BOOT="/isodevice"
FLAGS=$(cat ${BASE}/flags) FETCHER_FLAGS="$(cat /icon/config/fetcher.flags)"
if -f /icon/config/update_grub.flags; then
UPDATE_GRUB_FLAGS="$(cat /icon/config/update_grub.flags)"
fi
CA_CERT="/icon/config/ca.image.cert.pem"
${BASE}/fetcher.py --image-dir="${IMAGES}" --ca-cert=${BASE}/../config/ca.cert.pem ${FLAGS} HTTPS_CLIENT_KEY="/systemid/$(hostname).www.key.pem"
${BASE}/update_grub.py --image-dir="${IMAGES}" --boot-dir="${BOOT}" > ${BOOT}/grub/grub.cfg.tmp && mv ${BOOT}/grub/grub.cfg.tmp ${BOOT}/grub/grub.cfg HTTPS_CLIENT_CERT="/systemid/$(hostname).www.cert.pem"
HTTPS_CA_CERT="/icon/config/ca.www.cert.pem"
if test -e "${HTTPS_CLIENT_KEY}" -a -e "${HTTPS_CLIENT_CERT}"; then
HTTPS_CLIENT_FLAGS="--https-client-cert=${HTTPS_CLIENT_CERT} --https-client-key=${HTTPS_CLIENT_KEY}"
fi
if test -e "${HTTPS_CA_CERT}"; then
HTTPS_CA_FLAGS="--https-ca-cert=${HTTPS_CA_CERT}"
fi
"${BASE}/fetcher.py" --image-dir="${IMAGES}" --ca-cert="${CA_CERT}" ${FETCHER_FLAGS} ${HTTPS_CLIENT_FLAGS} ${HTTPS_CA_FLAGS}
"${BASE}/update_grub.py" --image-dir="${IMAGES}" --boot-dir="${BOOT}" ${UPDATE_GRUB_FLAGS} > "${BOOT}/grub/grub.cfg.tmp" && mv "${BOOT}/grub/grub.cfg.tmp" "${BOOT}/grub/grub.cfg"

View File

@@ -25,6 +25,10 @@ parser.add_argument(
dest='image_dir', dest='image_dir',
action='store', action='store',
required=True) required=True)
parser.add_argument(
'--kernel-arg',
dest='kernel_args',
action='append')
parser.add_argument( parser.add_argument(
'--module', '--module',
dest='modules', dest='modules',
@@ -35,8 +39,13 @@ parser.add_argument(
action='store', action='store',
required=True) required=True)
parser.add_argument( parser.add_argument(
'--shell', '--post-module-shell',
dest='shell', dest='post_module_shell',
action='store_true',
default=False)
parser.add_argument(
'--pre-module-shell',
dest='pre_module_shell',
action='store_true', action='store_true',
default=False) default=False)
parser.add_argument( parser.add_argument(
@@ -55,7 +64,6 @@ class ImageBuilder(object):
'iputils-ping', 'iputils-ping',
'linux-firmware', 'linux-firmware',
'linux-firmware-nonfree', 'linux-firmware-nonfree',
'openssh-server',
'ubuntu-minimal', 'ubuntu-minimal',
'ubuntu-standard', 'ubuntu-standard',
'user-setup', 'user-setup',
@@ -83,13 +91,14 @@ class ImageBuilder(object):
'loopback.cfg': 'boot/grub/loopback.cfg', 'loopback.cfg': 'boot/grub/loopback.cfg',
} }
def __init__(self, source_iso, image_dir, archive, arch, release, modules): def __init__(self, source_iso, image_dir, archive, arch, release, modules, kernel_args):
self._source_iso = source_iso self._source_iso = source_iso
self._image_dir = image_dir self._image_dir = image_dir
self._archive = archive self._archive = archive
self._arch = arch self._arch = arch
self._release = release self._release = release
self._modules = modules or [] self._modules = modules or []
self._kernel_args = kernel_args or []
self._ico_server_path = os.path.dirname(sys.argv[0]) self._ico_server_path = os.path.dirname(sys.argv[0])
@@ -100,6 +109,27 @@ class ImageBuilder(object):
def _ExecChroot(self, chroot_path, *args, **kwargs): def _ExecChroot(self, chroot_path, *args, **kwargs):
self._Exec('chroot', chroot_path, *args, **kwargs) self._Exec('chroot', chroot_path, *args, **kwargs)
def _CreateRoot(self, timestamp):
root = tempfile.mkdtemp()
self._rmtree.append(root)
self._Exec(
'mount',
'--types', 'tmpfs',
'none',
root)
self._umount.append(root)
return root
def _MountProc(self, chroot_path):
path = os.path.join(chroot_path, 'proc')
self._Exec(
'mount',
'--types', 'proc',
'none',
path)
self._umount.append(path)
return path
def _Debootstrap(self, root): def _Debootstrap(self, root):
path = os.path.join(root, 'chroot') path = os.path.join(root, 'chroot')
os.mkdir(path) os.mkdir(path)
@@ -219,6 +249,12 @@ class ImageBuilder(object):
source) source)
os.unlink(os.path.join(chroot_path, 'usr', 'sbin', 'policy-rc.d')) os.unlink(os.path.join(chroot_path, 'usr', 'sbin', 'policy-rc.d'))
def _Unmount(self, path):
self._Exec(
'umount',
path)
self._umount.remove(path)
def _Squash(self, chroot_path, union_path): def _Squash(self, chroot_path, union_path):
self._Exec( self._Exec(
'mksquashfs', 'mksquashfs',
@@ -228,9 +264,11 @@ class ImageBuilder(object):
def _CopyISOFiles(self, union_path): def _CopyISOFiles(self, union_path):
for source, dest in self._ISO_COPIES.items(): for source, dest in self._ISO_COPIES.items():
shutil.copyfile( source_path = os.path.join(self._ico_server_path, 'iso_files', source)
os.path.join(self._ico_server_path, 'iso_files', source), dest_path = os.path.join(union_path, dest)
os.path.join(union_path, dest)) with open(source_path, 'r') as source_fh, open(dest_path, 'w') as dest_fh:
for line in source_fh:
dest_fh.write(line.replace('$KERNEL_ARGS', ' '.join(self._kernel_args)))
def _CreateISO(self, union_path, timestamp): def _CreateISO(self, union_path, timestamp):
dest_iso = os.path.join(self._image_dir, '%d.iso' % timestamp) dest_iso = os.path.join(self._image_dir, '%d.iso' % timestamp)
@@ -241,31 +279,25 @@ class ImageBuilder(object):
return dest_iso return dest_iso
def _BuildImage(self): def _BuildImage(self):
root = tempfile.mkdtemp()
self._rmtree.append(root)
timestamp = int(time.time()) timestamp = int(time.time())
root = self._CreateRoot(timestamp)
print('Building image in:', root) print('Building image in:', root)
self._Exec(
'mount',
'--types', 'tmpfs',
'none',
root)
self._umount.append(root)
chroot_path = self._Debootstrap(root) chroot_path = self._Debootstrap(root)
union_path = self._CreateUnion(root) union_path = self._CreateUnion(root)
proc_path = self._MountProc(chroot_path)
self._FixSourcesList(chroot_path) self._FixSourcesList(chroot_path)
self._AddDiversions(chroot_path) self._AddDiversions(chroot_path)
self._InstallPackages(chroot_path) self._InstallPackages(chroot_path)
self._WriteVersion(chroot_path, timestamp) self._WriteVersion(chroot_path, timestamp)
if FLAGS.pre_module_shell:
self._Exec('bash', cwd=root)
self._RunModules(chroot_path) self._RunModules(chroot_path)
self._CleanPackages(chroot_path) self._CleanPackages(chroot_path)
self._RemoveDiversions(chroot_path) self._RemoveDiversions(chroot_path)
if FLAGS.shell: if FLAGS.post_module_shell:
self._Exec('bash', cwd=root) self._Exec('bash', cwd=root)
self._Unmount(proc_path)
self._Squash(chroot_path, union_path) self._Squash(chroot_path, union_path)
self._CopyISOFiles(union_path) self._CopyISOFiles(union_path)
iso_path = self._CreateISO(union_path, timestamp) iso_path = self._CreateISO(union_path, timestamp)
@@ -297,7 +329,8 @@ def main():
FLAGS.archive, FLAGS.archive,
FLAGS.arch, FLAGS.arch,
FLAGS.release, FLAGS.release,
FLAGS.modules) FLAGS.modules,
FLAGS.kernel_args)
builder.BuildImage() builder.BuildImage()

View File

@@ -43,14 +43,14 @@ class ManifestBuilder(object):
self._image_dir = image_dir self._image_dir = image_dir
self._old_manifest = old_manifest self._old_manifest = old_manifest
def _Rollouts(self): def _OldImages(self):
if not self._old_manifest: if not self._old_manifest:
return {} return {}
try: try:
with open(self._old_manifest, 'r') as fh: with open(self._old_manifest, 'r') as fh:
parsed = json.load(fh) parsed = json.load(fh)
return dict( return dict(
(image['timestamp'], image['rollout_‱']) (image['timestamp'], image)
for image in parsed['images']) for image in parsed['images'])
except FileNotFoundError: except FileNotFoundError:
return {} return {}
@@ -60,7 +60,7 @@ class ManifestBuilder(object):
'timestamp': int(time.time()), 'timestamp': int(time.time()),
'images': [], 'images': [],
} }
rollouts = self._Rollouts() old_images = self._OldImages()
for filename in os.listdir(self._image_dir): for filename in os.listdir(self._image_dir):
match = self._FILE_REGEX.match(filename) match = self._FILE_REGEX.match(filename)
if not match: if not match:
@@ -68,8 +68,15 @@ class ManifestBuilder(object):
timestamp = int(match.group('timestamp')) timestamp = int(match.group('timestamp'))
image = { image = {
'timestamp': timestamp, 'timestamp': timestamp,
'rollout_‱': rollouts.get(timestamp, FLAGS.default_rollout), 'rollout_‱': FLAGS.default_rollout,
} }
ret['images'].append(image)
old_image = old_images.get(timestamp)
if old_image:
image.update(old_image)
continue
with open(os.path.join(self._image_dir, filename), 'rb') as fh: with open(os.path.join(self._image_dir, filename), 'rb') as fh:
hash_obj = hashlib.sha256() hash_obj = hashlib.sha256()
while True: while True:
@@ -78,7 +85,7 @@ class ManifestBuilder(object):
break break
hash_obj.update(data) hash_obj.update(data)
image['hash'] = hash_obj.hexdigest() image['hash'] = hash_obj.hexdigest()
ret['images'].append(image)
ret['images'].sort(key=lambda x: x['timestamp'], reverse=True) ret['images'].sort(key=lambda x: x['timestamp'], reverse=True)
return ret return ret

View File

@@ -2,6 +2,6 @@ set timeout=5
terminal_output console terminal_output console
menuentry "Ubuntu Server Live Image" { menuentry "Ubuntu Server Live Image" {
linux /casper/vmlinuz.efi boot=casper root=LABEL=ISOIMAGE linux /casper/vmlinuz.efi boot=casper root=LABEL=ISOIMAGE nomodeset $KERNEL_ARGS
initrd /casper/initrd.lz initrd /casper/initrd.lz
} }

View File

@@ -2,6 +2,6 @@ set timeout=5
terminal_output console terminal_output console
menuentry "Ubuntu Server Live Image" { menuentry "Ubuntu Server Live Image" {
linux /casper/vmlinuz.efi boot=casper iso-scan/filename=${iso_path} linux /casper/vmlinuz.efi boot=casper iso-scan/filename=${iso_path} nomodeset $KERNEL_ARGS
initrd /casper/initrd.lz initrd /casper/initrd.lz
} }

View File

@@ -28,6 +28,18 @@ parser.add_argument(
dest='device', dest='device',
action='store', action='store',
required=True) required=True)
parser.add_argument(
'--https-ca-cert',
dest='https_ca_cert',
action='store')
parser.add_argument(
'--https-client-cert',
dest='https_client_cert',
action='store')
parser.add_argument(
'--https-client-key',
dest='https_client_key',
action='store')
parser.add_argument( parser.add_argument(
'--persistent-percent', '--persistent-percent',
dest='persistent_percent', dest='persistent_percent',
@@ -51,18 +63,46 @@ def main():
'apt-get', 'apt-get',
'install', 'install',
'--assume-yes', '--assume-yes',
'git', 'grub-pc', 'python3-openssl') 'git', 'grub-pc', 'python3-openssl', 'python3-requests')
ExecChroot( os.makedirs(os.path.join(FLAGS.chroot_path, 'icon', 'config'), exist_ok=True)
'git',
'clone', if not os.path.exists(os.path.join(FLAGS.chroot_path, 'icon', 'iconograph')):
'https://github.com/robot-tools/iconograph.git', ExecChroot(
'autoimage') 'git',
'clone',
'https://github.com/robot-tools/iconograph.git',
'icon/iconograph')
os.mkdir(os.path.join(FLAGS.chroot_path, 'autoimage', 'config'))
shutil.copyfile( shutil.copyfile(
FLAGS.ca_cert, FLAGS.ca_cert,
os.path.join(FLAGS.chroot_path, 'autoimage', 'config', 'ca.cert.pem')) os.path.join(FLAGS.chroot_path, 'icon', 'config', 'ca.image.cert.pem'))
image_flags = []
if FLAGS.https_ca_cert:
https_ca_cert_path = os.path.join('icon', 'config', 'ca.www.cert.pem')
shutil.copyfile(
FLAGS.https_ca_cert,
os.path.join(FLAGS.chroot_path, https_ca_cert_path))
image_flags.extend([
'--https-ca-cert', os.path.join('/', https_ca_cert_path),
])
if FLAGS.https_client_cert and FLAGS.https_client_key:
https_client_cert_path = os.path.join('icon', 'config', 'client.www.cert.pem')
shutil.copyfile(
FLAGS.https_client_cert,
os.path.join(FLAGS.chroot_path, https_client_cert_path))
https_client_key_path = os.path.join('icon', 'config', 'client.www.key.pem')
shutil.copyfile(
FLAGS.https_client_key,
os.path.join(FLAGS.chroot_path, https_client_key_path))
os.chmod(os.path.join(FLAGS.chroot_path, https_client_key_path), 0o400)
image_flags.extend([
'--https-client-cert', os.path.join('/', https_client_cert_path),
'--https-client-key', os.path.join('/', https_client_key_path),
])
parsed = parse.urlparse(FLAGS.base_url) parsed = parse.urlparse(FLAGS.base_url)
@@ -71,23 +111,22 @@ def main():
fh.write(""" fh.write("""
description "AutoImage" description "AutoImage"
start on stopped rc RUNLEVEL=[2345] start on runlevel [2345]
stop on runlevel [!2345]
script script
chvt 7 exec </dev/tty8 >/dev/tty8 2>&1
/autoimage/client/wait_for_service.py --host=%(host)s --service=%(service)s </dev/tty7 >/dev/tty7 2>&1 chvt 8
chvt 7 /icon/iconograph/client/wait_for_service.py --host=%(host)s --service=%(service)s
/autoimage/imager/image.py --device=%(device)s --persistent-percent=%(persistent_percent)d --ca-cert=/autoimage/config/ca.cert.pem --base-url=%(base_url)s </dev/tty7 >/dev/tty7 2>&1 chvt 8
chvt 7 /icon/iconograph/imager/image.py --device=%(device)s --persistent-percent=%(persistent_percent)d --ca-cert=/icon/config/ca.image.cert.pem --base-url=%(base_url)s %(image_flags)s
chvt 8
echo >/dev/tty7 echo
echo "==================" >/dev/tty7 echo "=================="
echo "autoimage complete" >/dev/tty7 echo "autoimage complete"
echo "==================" >/dev/tty7 echo "=================="
/autoimage/client/alert.py --type=happy </dev/tty7 >/dev/tty7 /icon/iconograph/client/alert.py --type=happy
end script end script
""" % { """ % {
'host': parsed.hostname, 'host': parsed.hostname,
@@ -95,6 +134,7 @@ end script
'device': FLAGS.device, 'device': FLAGS.device,
'persistent_percent': FLAGS.persistent_percent, 'persistent_percent': FLAGS.persistent_percent,
'base_url': FLAGS.base_url, 'base_url': FLAGS.base_url,
'image_flags': ' '.join(image_flags),
}) })

144
server/modules/certclient.py Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/python3
import argparse
import os
import shutil
import subprocess
from urllib import parse
parser = argparse.ArgumentParser(description='iconograph autoimage')
parser.add_argument(
'--chroot-path',
dest='chroot_path',
action='store',
required=True)
parser.add_argument(
'--ca-cert',
dest='ca_cert',
action='store',
required=True)
parser.add_argument(
'--client-cert',
dest='client_cert',
action='store',
required=True)
parser.add_argument(
'--client-key',
dest='client_key',
action='store',
required=True)
parser.add_argument(
'--subject',
dest='subject',
action='store',
required=True)
parser.add_argument(
'--tag',
dest='tag',
action='store',
required=True)
parser.add_argument(
'--server',
dest='server',
action='store',
required=True)
FLAGS = parser.parse_args()
def Exec(*args, **kwargs):
print('+', args)
subprocess.check_call(args, **kwargs)
def ExecChroot(*args, **kwargs):
Exec('chroot', FLAGS.chroot_path, *args, **kwargs)
def main():
ExecChroot(
'apt-get',
'install',
'--assume-yes',
'git', 'python3-requests', 'openssl')
os.makedirs(os.path.join(FLAGS.chroot_path, 'icon', 'config'), exist_ok=True)
if not os.path.exists(os.path.join(FLAGS.chroot_path, 'icon', 'iconograph')):
ExecChroot(
'git',
'clone',
'https://github.com/robot-tools/iconograph.git',
'icon/iconograph')
if not os.path.exists(os.path.join(FLAGS.chroot_path, 'icon', 'certserver')):
ExecChroot(
'git',
'clone',
'https://github.com/robot-tools/certserver.git',
'icon/certserver')
ca_cert_path = os.path.join('icon', 'config', 'ca.%s.certserver.cert.pem' % FLAGS.tag)
shutil.copyfile(
FLAGS.ca_cert,
os.path.join(FLAGS.chroot_path, ca_cert_path))
client_cert_path = os.path.join('icon', 'config', 'client.%s.certserver.cert.pem' % FLAGS.tag)
shutil.copyfile(
FLAGS.client_cert,
os.path.join(FLAGS.chroot_path, client_cert_path))
client_key_path = os.path.join('icon', 'config', 'client.%s.certserver.key.pem' % FLAGS.tag)
shutil.copyfile(
FLAGS.client_key,
os.path.join(FLAGS.chroot_path, client_key_path))
os.chmod(os.path.join(FLAGS.chroot_path, client_key_path), 0o400)
parsed = parse.urlparse(FLAGS.server)
init = os.path.join(FLAGS.chroot_path, 'etc', 'init', 'certclient.%s.conf' % FLAGS.tag)
with open(init, 'w') as fh:
fh.write("""
description "CertClient %(tag)s"
start on systemid-ready
script
exec </dev/tty9 >/dev/tty9 2>&1
chvt 9
KEY="/systemid/$(hostname).%(tag)s.key.pem"
CERT="/systemid/$(hostname).%(tag)s.cert.pem"
SUBJECT="$(echo '%(subject)s' | sed s/SYSTEMID/$(hostname)/g)"
if test ! -s "${KEY}"; then
openssl ecparam -name secp384r1 -genkey | openssl ec -out "${KEY}"
chmod 0400 "${KEY}"
fi
chvt 9
/icon/iconograph/client/wait_for_service.py --host=%(host)s --service=%(service)s
chvt 9
if test ! -s "${CERT}"; then
openssl req -new -key "${KEY}" -subj "${SUBJECT}" | /icon/certserver/certclient.py --ca-cert=/icon/config/ca.%(tag)s.certserver.cert.pem --client-cert=/icon/config/client.%(tag)s.certserver.cert.pem --client-key=/icon/config/client.%(tag)s.certserver.key.pem --server=%(server)s > "${CERT}"
chmod 0444 "${CERT}"
fi
chvt 9
echo
echo "=================="
echo "certclient %(tag)s complete"
echo "=================="
end script
""" % {
'host': parsed.hostname,
'service': parsed.port or parsed.scheme,
'subject': FLAGS.subject,
'tag': FLAGS.tag,
'server': FLAGS.server,
})
if __name__ == '__main__':
main()

View File

@@ -22,6 +22,10 @@ parser.add_argument(
dest='chroot_path', dest='chroot_path',
action='store', action='store',
required=True) required=True)
parser.add_argument(
'--https-ca-cert',
dest='https_ca_cert',
action='store')
parser.add_argument( parser.add_argument(
'--max-images', '--max-images',
dest='max_images', dest='max_images',
@@ -45,19 +49,28 @@ def main():
'apt-get', 'apt-get',
'install', 'install',
'--assume-yes', '--assume-yes',
'daemontools-run', 'git', 'python3-openssl') 'daemontools-run', 'git', 'python3-openssl', 'python3-requests')
ExecChroot( os.makedirs(os.path.join(FLAGS.chroot_path, 'icon', 'config'), exist_ok=True)
'git',
'clone', if not os.path.exists(os.path.join(FLAGS.chroot_path, 'icon', 'iconograph')):
'https://github.com/robot-tools/iconograph.git') ExecChroot(
'git',
'clone',
'https://github.com/robot-tools/iconograph.git',
'icon/iconograph')
os.mkdir(os.path.join(FLAGS.chroot_path, 'iconograph', 'config'))
shutil.copyfile( shutil.copyfile(
FLAGS.ca_cert, FLAGS.ca_cert,
os.path.join(FLAGS.chroot_path, 'iconograph', 'config', 'ca.cert.pem')) os.path.join(FLAGS.chroot_path, 'icon', 'config', 'ca.image.cert.pem'))
path = os.path.join(FLAGS.chroot_path, 'iconograph', 'client', 'flags') if FLAGS.https_ca_cert:
shutil.copyfile(
FLAGS.https_ca_cert,
os.path.join(FLAGS.chroot_path, 'icon', 'config', 'ca.www.cert.pem'))
path = os.path.join(FLAGS.chroot_path, 'icon', 'config', 'fetcher.flags')
with open(path, 'w') as fh: with open(path, 'w') as fh:
fh.write('--base-url=%(base_url)s --max-images=%(max_images)d\n' % { fh.write('--base-url=%(base_url)s --max-images=%(max_images)d\n' % {
'base_url': FLAGS.base_url, 'base_url': FLAGS.base_url,
@@ -65,8 +78,8 @@ def main():
}) })
os.symlink( os.symlink(
'/iconograph/client', '/icon/iconograph/client',
os.path.join(FLAGS.chroot_path, 'etc', 'service', 'iconograph')) os.path.join(FLAGS.chroot_path, 'etc', 'service', 'iconograph-client'))
if __name__ == '__main__': if __name__ == '__main__':

43
server/modules/openssh.py Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/python3
import argparse
import glob
import os
import subprocess
parser = argparse.ArgumentParser(description='iconograph openssh')
parser.add_argument(
'--chroot-path',
dest='chroot_path',
action='store',
required=True)
FLAGS = parser.parse_args()
def Exec(*args, **kwargs):
print('+', args)
subprocess.check_call(args, **kwargs)
def ExecChroot(*args, **kwargs):
Exec('chroot', FLAGS.chroot_path, *args, **kwargs)
def main():
ExecChroot(
'apt-get',
'install',
'--assume-yes',
'openssh-server')
for path in glob.glob(os.path.join(FLAGS.chroot_path, 'etc', 'ssh', 'ssh_host_*')):
os.unlink(path)
os.symlink(
'/systemid/ssh_host_ed25519_key',
os.path.join(FLAGS.chroot_path, 'etc', 'ssh', 'ssh_host_ed25519_key'))
if __name__ == '__main__':
main()

View File

@@ -22,9 +22,13 @@ def main():
description "Mount /persistent" description "Mount /persistent"
start on filesystem start on filesystem
task
emits persistent-ready
script script
mount LABEL=PERSISTENT /persistent mount LABEL=PERSISTENT /persistent
initctl emit --no-wait persistent-ready
end script end script
""") """)

View File

@@ -22,6 +22,9 @@ def main():
description "Mount /systemid" description "Mount /systemid"
start on filesystem start on filesystem
task
emits systemid-ready
script script
mount LABEL=SYSTEMID /systemid mount LABEL=SYSTEMID /systemid
@@ -29,6 +32,7 @@ script
echo ${SYSTEMID} > /etc/hostname echo ${SYSTEMID} > /etc/hostname
hostname --file /etc/hostname hostname --file /etc/hostname
grep ${SYSTEMID} /etc/hosts >/dev/null || echo "127.0.2.1 ${SYSTEMID}" >> /etc/hosts grep ${SYSTEMID} /etc/hosts >/dev/null || echo "127.0.2.1 ${SYSTEMID}" >> /etc/hosts
initctl emit --no-wait systemid-ready
end script end script
""") """)

View File

@@ -121,6 +121,14 @@ SYSTEMID=%(system_id)s
'system_id': new_id, 'system_id': new_id,
}) })
def _GenerateSSHKey(self, root):
self._Exec(
'ssh-keygen',
'-f', os.path.join(root, 'ssh_host_ed25519_key'),
'-N', '',
'-t', 'ed25519',
)
def _Image(self): def _Image(self):
self._PartitionAndMkFS() self._PartitionAndMkFS()
root = self._Mount() root = self._Mount()
@@ -136,6 +144,7 @@ New ID: \033[91m%s\033[00m
============== ==============
""" % new_id) """ % new_id)
self._GenerateSSHKey(root)
def Image(self): def Image(self):
self._umount = [] self._umount = []

70
util/fetch_archive.py Executable file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/python3
import argparse
import os
import requests
import shutil
import subprocess
parser = argparse.ArgumentParser(description='iconograph fetch_archive')
parser.add_argument(
'--dest-dir',
dest='dest_dir',
action='store',
default='.')
parser.add_argument(
'--https-ca-cert',
dest='https_ca_cert',
action='store')
parser.add_argument(
'--https-client-cert',
dest='https_client_cert',
action='store')
parser.add_argument(
'--https-client-key',
dest='https_client_key',
action='store')
parser.add_argument(
'--url',
dest='url',
action='store',
required=True)
FLAGS = parser.parse_args()
class ArchiveFetcher(object):
_BUF_SIZE = 2 ** 16
def __init__(self, https_ca_cert, https_client_cert, https_client_key):
self._session = requests.Session()
if https_ca_cert:
self._session.verify = https_ca_cert
if https_client_cert and https_client_key:
self._session.cert = (https_client_cert, https_client_key)
def Fetch(self, url, dest_dir='.'):
resp = self._session.get(url, stream=True)
tar = subprocess.Popen(
['tar', '--extract'],
stdin=subprocess.PIPE,
cwd=dest_dir)
for data in resp.iter_content(self._BUF_SIZE):
tar.stdin.write(data)
tar.wait()
def main():
fetcher = ArchiveFetcher(
FLAGS.https_ca_cert,
FLAGS.https_client_cert,
FLAGS.https_client_key)
fetcher.Fetch(
FLAGS.url,
FLAGS.dest_dir)
if __name__ == '__main__':
main()

BIN
www/code-39.woff Normal file

Binary file not shown.

101
www/systemid.html Normal file
View File

@@ -0,0 +1,101 @@
<html>
<head>
<title>SystemID</title>
<script src="https://use.typekit.net/hsz4ulo.js"></script>
<script>try{Typekit.load({ async: true });}catch(e){}</script>
<style>
@font-face {
font-family: 'code-39';
src: url('code-39.woff') format('woff');
font-weight: normal;
font-style: normal;
}
.barcode {
font-family: 'code-39';
font-size: 24px;
}
.top {
text-align: left;
}
.bottom {
text-align: right;
}
[contenteditable=true]:empty::before{
content: attr(placeholder);
color: grey;
}
@media screen {
.sysid {
border: 1px dashed #AAA;
}
}
.sysid {
font-family: 'droid-sans-mono';
font-size: 140px;
text-align: center;
text-transform: uppercase;
}
.sysid:focus {
outline: 0;
}
.block {
width: 600px;
margin: 20px;
}
</style>
</head>
<body>
<div class="block">
<div id="barcode0.0" class="top barcode"></div>
<div id="sysid0" class="sysid" contenteditable="true" placeholder="SYSID#"></div>
<div id="barcode0.1" class="bottom barcode"></div>
</div>
<div class="block">
<div id="barcode1.0" class="top barcode"></div>
<div id="sysid1" class="sysid" contenteditable="true" placeholder="SYSID#"></div>
<div id="barcode1.1" class="bottom barcode"></div>
</div>
<div class="block">
<div id="barcode2.0" class="top barcode"></div>
<div id="sysid2" class="sysid" contenteditable="true" placeholder="SYSID#"></div>
<div id="barcode2.1" class="bottom barcode"></div>
</div>
<script>
"use strict";
document.addEventListener('DOMContentLoaded', (e) => {
for (let i = 0; true; i++) {
let sysid = document.getElementById('sysid' + i);
if (!sysid) {
break;
}
let barcodes = [];
for (let j = 0; true; j++) {
let barcode = document.getElementById('barcode' + i + '.' + j);
if (!barcode) {
break;
}
barcodes.push(barcode);
barcode.innerText = '**';
}
sysid.addEventListener('input', (e) => {
let barcodeText = '*' + sysid.innerText.toUpperCase() + '*';
barcodes.forEach((barcode) => {
barcode.innerText = barcodeText;
});
});
}
});
</script>
</body>
</html>