still doing some work but checking in what i have so far

This commit is contained in:
brent s 2019-10-09 07:18:10 -04:00
parent 3ca56d7b5c
commit 108588827a
8 changed files with 259 additions and 70 deletions

72
aif.xsd
View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://aif.square-r00t.net"
xmlns="http://aif.square-r00t.net"
elementFormDefault="qualified">
targetNamespace="http://aif-ng.io/"
xmlns="http://aif-ng.io/"
xmlns:aif="http://aif-ng.io/"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:annotation>
<xs:documentation>
See https://aif.square-r00t.net/ for more information about this project.
@ -148,22 +150,24 @@
<xs:sequence>
<xs:element name="part" minOccurs="1" maxOccurs="unbounded">
<xs:complexType>
<!-- num should not be required since it's a sequence; it's inherently
ordered! -->
<xs:attribute name="num" type="xs:positiveInteger" use="required"/>
<xs:attribute name="start" type="disksize" use="required"/>
<xs:attribute name="stop" type="disksize" use="required"/>
<xs:attribute name="fstype" type="fstype" use="required"/>
<xs:attribute name="start" type="aif:disksize" use="required"/>
<xs:attribute name="stop" type="aif:disksize" use="required"/>
<xs:attribute name="fstype" type="aif:fstype" use="required"/>
</xs:complexType>
<xs:unique name="unique-partnum">
<xs:selector xpath="part"/>
<xs:selector xpath="aif:part"/>
<xs:field xpath="@num"/>
</xs:unique>
</xs:element>
</xs:sequence>
<xs:attribute name="device" type="diskdev" use="required"/>
<xs:attribute name="diskfmt" type="diskfmt" use="required"/>
<xs:attribute name="device" type="aif:diskdev" use="required"/>
<xs:attribute name="diskfmt" type="aif:diskfmt" use="required"/>
</xs:complexType>
<xs:unique name="unique-diskdev">
<xs:selector xpath="disk"/>
<xs:selector xpath="aif:disk"/>
<xs:field xpath="@device"/>
</xs:unique>
</xs:element>
@ -171,13 +175,13 @@
<xs:element name="mount" minOccurs="1" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="order" type="xs:integer" use="required"/>
<xs:attribute name="source" type="diskdev" use="required"/>
<xs:attribute name="source" type="aif:diskdev" use="required"/>
<xs:attribute name="target" type="xs:token" use="required"/>
<xs:attribute name="fstype" type="fstype"/>
<xs:attribute name="opts" type="mntopts"/>
<xs:attribute name="fstype" type="aif:fstype"/>
<xs:attribute name="opts" type="aif:mntopts"/>
</xs:complexType>
<xs:unique name="unique-mnts">
<xs:selector xpath="mount"/>
<xs:selector xpath="aif:mount"/>
<xs:field xpath="@order"/>
<xs:field xpath="@source"/>
<xs:field xpath="@target"/>
@ -194,10 +198,10 @@
<xs:sequence>
<xs:element name="iface" minOccurs="1" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="device" type="iface" use="required"/>
<xs:attribute name="address" type="netaddress" use="required"/>
<xs:attribute name="netproto" type="netproto" use="required"/>
<xs:attribute name="gateway" type="netaddress"/>
<xs:attribute name="device" type="aif:iface" use="required"/>
<xs:attribute name="address" type="aif:netaddress" use="required"/>
<xs:attribute name="netproto" type="aif:netproto" use="required"/>
<xs:attribute name="gateway" type="aif:netaddress"/>
<xs:attribute name="resolvers" type="xs:string"/>
</xs:complexType>
</xs:element>
@ -205,7 +209,7 @@
<xs:attribute name="hostname" type="xs:token" use="required"/>
</xs:complexType>
<xs:unique name="unique-iface">
<xs:selector xpath="iface"/>
<xs:selector xpath="aif:iface"/>
<xs:field xpath="@address"/>
<xs:field xpath="@netproto"/>
</xs:unique>
@ -229,30 +233,30 @@
</xs:element>
<xs:element name="xgroup" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="name" type="nixgroup" use="required"/>
<xs:attribute name="name" type="aif:nixgroup" use="required"/>
<xs:attribute name="create" type="xs:boolean"/>
<xs:attribute name="gid" type="xs:boolean"/>
</xs:complexType>
<xs:unique name="unique-grp">
<xs:selector xpath="xgroup"/>
<xs:selector xpath="aif:xgroup"/>
<xs:field xpath="@name"/>
</xs:unique>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="uid" type="xs:token"/>
<xs:attribute name="group" type="nixgroup"/>
<xs:attribute name="group" type="aif:nixgroup"/>
<xs:attribute name="gid" type="xs:token"/>
<xs:attribute name="password" type="nixpass"/>
<xs:attribute name="password" type="aif:nixpass"/>
<xs:attribute name="comment" type="xs:token"/>
<xs:attribute name="sudo" type="xs:boolean"/>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="rootpass" type="nixpass"/>
<xs:attribute name="rootpass" type="aif:nixpass"/>
</xs:complexType>
<xs:unique name="unique-usr">
<xs:selector xpath="user"/>
<xs:selector xpath="aif:user"/>
<xs:field xpath="@name"/>
</xs:unique>
</xs:element>
@ -262,7 +266,7 @@
<xs:attribute name="status" type="xs:boolean" use="required"/>
</xs:complexType>
<xs:unique name="unique-svc">
<xs:selector xpath="service"/>
<xs:selector xpath="aif:service"/>
<xs:field xpath="@name"/>
<xs:field xpath="@status"/>
</xs:unique>
@ -288,7 +292,7 @@
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="enabled" type="xs:boolean" use="required"/>
<xs:attribute name="siglevel" type="xs:token" use="required"/>
<xs:attribute name="mirror" type="pacuri" use="required"/>
<xs:attribute name="mirror" type="aif:pacuri" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
@ -297,11 +301,11 @@
<xs:element name="mirrorlist" maxOccurs="1" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="mirror" type="pacuri" maxOccurs="unbounded" minOccurs="1"/>
<xs:element name="mirror" type="aif:pacuri" maxOccurs="unbounded" minOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:unique name="unique-mirrors">
<xs:selector xpath="mirror"/>
<xs:selector xpath="aif:mirror"/>
<xs:field xpath="."/>
</xs:unique>
</xs:element>
@ -325,7 +329,7 @@
<!-- BEGIN BOOTLOADER -->
<xs:element name="bootloader" maxOccurs="1" minOccurs="1">
<xs:complexType>
<xs:attribute name="type" type="bootloaders" use="required"/>
<xs:attribute name="type" type="aif:bootloaders" use="required"/>
<xs:attribute name="target" type="xs:token" use="required"/>
<xs:attribute name="efi" type="xs:boolean"/>
</xs:complexType>
@ -337,19 +341,19 @@
<xs:sequence>
<xs:element name="script" minOccurs="1" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="uri" type="scripturi" use="required"/>
<xs:attribute name="uri" type="aif:scripturi" use="required"/>
<xs:attribute name="order" type="xs:integer" use="required"/>
<xs:attribute name="execution" type="scripttype" use="required"/>
<xs:attribute name="execution" type="aif:scripttype" use="required"/>
<xs:attribute name="user" type="xs:string"/>
<xs:attribute name="password" type="xs:string"/>
<xs:attribute name="realm" type="xs:string"/>
<xs:attribute name="authtype" type="authselect"/>
<xs:attribute name="authtype" type="aif:authselect"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:unique name="unique-script">
<xs:selector xpath="script"/>
<xs:selector xpath="aif:script"/>
<xs:field xpath="@order"/>
</xs:unique>
</xs:element>

9
aif/aif_util.py Normal file
View File

@ -0,0 +1,9 @@
def xmlBool(xmlobj):
if isinstance(xmlobj, bool):
return (xmlobj)
if xmlobj.lower() in ('1', 'true'):
return(True)
elif xmlobj.lower() in ('0', 'false'):
return(False)
else:
return(None)

View File

@ -3,6 +3,9 @@
# https://github.com/dcantrell/pyparted/blob/master/examples/query_device_capacity.py
# TODO: Remember to replicate genfstab behaviour.

import os
import re
import subprocess
try:
# https://stackoverflow.com/a/34812552/733214
# https://github.com/karelzak/util-linux/blob/master/libmount/python/test_mount_context.py#L6
@ -11,5 +14,150 @@ except ImportError:
# We should never get here. util-linux is part of core (base) in Arch and uses "libmount".
import pylibmount as mount
##
import parted
import blkinfo
import parted # https://www.gnu.org/software/parted/api/index.html
import psutil
##
from .aif_util import xmlBool


# parted lib can do SI or IEC (see table to right at https://en.wikipedia.org/wiki/Binary_prefix)
# We bit-shift to do conversions:
# https://stackoverflow.com/a/12912296/733214
# https://stackoverflow.com/a/52684562/733214
_units = {'B': 0,
'kB': 7,
'MB': 17,
'GB': 27,
'TB': 37,
'KiB': 10,
'MiB': 20,
'GiB': 30,
'TiB': 40}
_pos_re = re.compile((r'^(?P<pos_or_neg>-|\+)?\s*'
r'(?P<size>[0-9]+)\s*'
# empty means size in sectors
r'(?P<pct_unit_or_sct>%|[{0}]|)\s*$'.format(''.join(list(_units.keys())))),
re.IGNORECASE)


def convertSizeUnit(pos):
orig_pos = pos
pos = _pos_re.search(pos)
if pos:
pos_or_neg = (pos.group('pos_or_neg') if pos.group('pos_or_neg') else None)
if pos_or_neg == '+':
from_beginning = True
elif pos_or_neg == '-':
from_beginning = False
else:
from_beginning = pos_or_neg
size = int(pos.group('size'))
amt_type = pos.group('pct_unit_or_sct').strip()
else:
raise ValueError('Invalid size specified: {0}'.format(orig_pos))
return((from_beginning, size, amt_type))


class Partition(object):
def __init__(self, disk_xml, diskobj, start_sector):
self.xml = disk_xml
device = diskobj.device
sizes = {}
for s in ('start', 'stop'):
x = dict(zip(('from_bgn', 'size', 'type'),
convertSizeUnit(self.xml.attrib[s])))
sectors = x['size']
if x['type'] == '%':
sectors = int(device.getLength() / x['size'])
elif x['type'] in _units.keys():
sectors = int(x['size'] << _units[x['type']] / device.sectorSize)
sizes[s] = (sectors, x['from_bgn'])
if sizes['start'][1] is not None:
if sizes['start'][1]:
self.begin = sizes['start'][0] + 0
else:
self.begin = device.getLength() - sizes['start'][0]
else:
self.begin = sizes['start'][0] + start_sector
if sizes['stop'][1] is not None:
if sizes['stop'][1]:
self.end = sizes['stop'][0] + 0
else:
self.end = device.getLength() - sizes['stop'][0]
else:
self.end = self.begin + sizes['stop'][0]
# TECHNICALLY we could craft the Geometry object with "length = ...", but it doesn't let us be explicit
# in configs. So we manually crunch the numbers and do it all at the end.
self.geometry = parted.Geometry(device = device,
start = self.begin,
end = self.end)
self.filesystem = parted.FileSystem(type = self.xml.attrib['fsType'],
)
self.partition = parted.Partition(disk = diskobj,
type = parted.PARTITION_NORMAL,
geometry = self.geometry,
fs = )

class Disk(object):
def __init__(self, disk_xml):
self.xml = disk_xml
self.devpath = self.xml.attrib['device']
self._initDisk()

def _initDisk(self):
self.device = parted.getDevice(self.devpath)
try:
self.disk = parted.newDisk(self.device)
if xmlBool(self.xml.attrib.get('forceReformat')):
self.is_lowformatted = False
self.is_hiformatted = False
else:
self.is_lowformatted = True
self.is_hiformatted = False
for d in blkinfo.BlkDiskInfo().get_disks(filters = {'group': 'disk',
'name': os.path.basename(self.devpath),
'kname': os.path.basename(self.devpath)}):
if d.get('fstype', '').strip() != '':
self.is_hiformatted = True
break
self.is_partitioned = True
except parted._ped.DiskException:
self.disk = None
self.is_lowformatted = False
self.is_hiformatted = False
self.is_partitioned = False
return()

def diskformat(self):
if self.is_lowformatted:
return()
# This is a safeguard. We do *not* want to low-format a disk that is mounted.
for p in psutil.disk_partitions(all = True):
if self.devpath in p:
raise RuntimeError('{0} is mounted; we are cowardly refusing to low-format it'.format(self.devpath))
self.disk.deleteAllPartitions()
tabletype = self.xml.attrib.get('diskFormat', 'gpt').lower()
if tabletype in ('bios', 'mbr'):
tabletype = 'msdos'
validlabels = parted.getLabels()
if tabletype not in validlabels:
raise ValueError(('Disk format {0} is not valid for this architecture;'
'must be one of: {1}'.format(tabletype, ', '.join(list(validlabels)))))
self.disk = parted.freshDisk(self.device, tabletype)

pass
self.is_lowformatted = True
self.is_partitioned = True
return()

def fsformat(self):
if self.is_hiformatted:
return()
# This is a safeguard. We do *not* want to high-format a disk that is mounted.
for p in psutil.disk_partitions(all = True):
if self.devpath in p:
raise RuntimeError('{0} is mounted; we are cowardly refusing to high-format it'.format(self.devpath))

pass
return()

View File

@ -7,13 +7,14 @@
import ensurepip
import json
import os
import tempfile
import subprocess
import sys
import tempfile
import venv

# TODO: a more consistent way of managing deps?
depmods = ['gpg', 'requests', 'lxml', 'psutil', 'pyparted', 'pytz', 'passlib', 'validators']
depmods = ['blkinfo', 'gpg', 'lxml', 'passlib', 'psutil',
'pyparted', 'pytz', 'requests', 'validators']

class EnvBuilder(object):
def __init__(self):

View File

@ -205,7 +205,7 @@ The `/aif/storage/disk` element holds information about disks on the system, and
|======================
^|Attribute ^|Value
^m|device |The disk to format (e.g. `/dev/sda`)
^m|diskfmt |https://en.wikipedia.org/wiki/GUID_Partition_Table[`gpt`^] or https://en.wikipedia.org/wiki/Master_boot_record[`bios`^]
^m|diskfmt |https://en.wikipedia.org/wiki/GUID_Partition_Table[`gpt`^] or https://en.wikipedia.org/wiki/Master_boot_record[`msdos`^]
|======================

===== `<part>`
@ -223,10 +223,11 @@ The `/aif/storage/disk/part` element holds information on partitioning that it's
[[specialsize]]
The `start` and `stop` attributes can be in the form of:

* A percentage, indicated by a percentage sign (`"10%"`)
* A size, indicated by the abbreviation (`"300K"`, `"30G"`, etc.)
** Accepts *K* (Kilobytes), *M* (Megabytes), *G* (Gigabytes), *T* (Terabytes), or *P* (Petabytes -- I know, I know.)
** Can also accept modifiers for this form (`"+500G"`, `"-400M"`)
* A percentage of the total disk size, indicated by a percentage sign (`"10%"`)
* A size, indicated by the abbreviation (`"300KiB"`, `"10GB"`, etc.)
** Accepts notation in https://en.wikipedia.org/wiki/Binary_prefix[SI or IEC formats^]
* A raw sector size, if no suffix is provided (sector sizes are *typically* 512 bytes but this can vary depending on disk) (`1024`)
* One can also specify modifiers (`"+10%"`, `"-400MB"`, etc.). A positive modifier indicates from the beginning of the *start of the disk* and a negative modifier specifies from the *end of the disk* (the default, if none is specified, is to use the _previously defined partition's end_ as the *start* for the new partition, or to use the _beginning of the usable disk space_ as the *start* if no previous partition is specified, and to *add* the size to the *start* until the *stop* is reached)

[[fstypes]]
NOTE: The following is a table for your reference of partition types. Note that it may be out of date, so reference the link above for the most up-to-date table.

View File

@ -1,3 +1,4 @@
- make disk partitioning/table formatting OPTIONAL (so it can be installed on an already formatted disk)
- support Arch Linux ARM?
- support multiple explicit locales via comma-separated list (see how i handle resolvers)
- config layout
@ -25,6 +26,7 @@
shm on /mnt/aif/dev/shm type tmpfs (rw,nosuid,nodev,relatime)
run on /mnt/aif/run type tmpfs (rw,nosuid,nodev,relatime,mode=755)
tmp on /mnt/aif/tmp type tmpfs (rw,nosuid,nodev)
OR just use pyalpm

DOCUMENTATION: aif-config.py (and note sample json as well)


View File

@ -1,54 +1,77 @@
<?xml version="1.0" encoding="UTF-8" ?>
<aif xmlns:aif="https://aif.square-r00t.net"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://aif.square-r00t.net aif.xsd">
<aif xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://aif-ng.io/"
xsi:schemaLocation="http://aif-ng.io/aif.xsd">
<storage>
<disk device="/dev/sda" diskfmt="gpt">
<part num="1" start="0%" stop="10%" fstype="ef00" />
<part num="2" start="10%" stop="80%" fstype="8300" />
<part num="3" start="80%" stop="100%" fstype="8200" />
<disk device="/dev/sda" diskFormat="gpt" forceReformat="true">
<!-- Partitions are numbered *in the order they are specified*. -->
<part id="boot" label="/boot" start="0%" stop="10%" fsType="ef00" /><!-- e.g. this would be /dev/sda1 -->
<part id="secrets1" label="shh" start="10%" stop="20%" fsType="8300"/>
<part id="lvm_member1" label="dynamic" start="20%" stop="30%" fsType="8300"/>
<part id="raid1_d1" start="30%" stop="55%" fsType="fd00"/>
<part id="raid1_d2" start="55%" stop="80%" fsType="fd00"/>
<part id="swap" start="80%" stop="100%" fsType="8200" />
</disk>
<mount source="/dev/sda2" target="/mnt/aif" order="1" />
<mount source="/dev/sda1" target="/mnt/aif/boot" order="2" />
<mount source="/dev/sda3" target="swap" order="3" />
<!-- "Special" devices are processed *in the order they are specified*. This is important if you wish to
e.g. layer LUKS on top of LVM - you would specify <lvm> before <luks> and reference the
<luksDev id="SOMETHING" ... > as <lvmLogical source="SOMETHING" ... />. -->
<luks>
<luksDev id="luks_secrets" name="secrets" source="secrets1" secret="superSeekritPassword"/>
</luks>
<lvm>
<lvmGroup id="vg1" name="GroupName">
<lvmLogical id="lv1" name="LogicalName" source="lvm_member1"/>
</lvmGroup>
</lvm>
<mdadm>
<array id="mdadm1" name="extra_data" meta="1.2" level="1">
<member source="raid1_d1"/>
<member source="raid1_d2"/>
</array>
</mdadm>
<!-- And you use the id to reference mountpoints as well. -->
<mount source="luks_secrets" target="/mnt/aif" order="1" />
<mount source="boot" target="/mnt/aif/boot" order="2" />
<mount source="swap" target="swap" order="3" />
<mount source="vg1" target="/mnt/aif/mnt/pool" order="4" />
<mount source="mdadm1" target="/mnt/aif/mnt/raid" order="5" />
</storage>
<network hostname="aiftest.square-r00t.net">
<iface device="auto" address="auto" netproto="ipv4" />
<iface device="auto" address="auto" netProto="ipv4" />
</network>
<system timezone="EST5EDT" locale="en_US.UTF-8" chrootpath="/mnt/aif" reboot="0">
<system timezone="EST5EDT" locale="en_US.UTF-8" chrootPath="/mnt/aif" reboot="0">
<!-- note: all password hashes below are "test"; don't waste your time trying to crack. :) -->
<users rootpass="$6$3YPpiS.l3SQC6ELe$NQ4qMvcDpv5j1cCM6AGNc5Hyg.rsvtzCt2VWlSbuZXCGg2GB21CMUN8TMGS35tdUezZ/n9y3UFGlmLRVWXvZR.">
<users rootPass="$6$3YPpiS.l3SQC6ELe$NQ4qMvcDpv5j1cCM6AGNc5Hyg.rsvtzCt2VWlSbuZXCGg2GB21CMUN8TMGS35tdUezZ/n9y3UFGlmLRVWXvZR.">
<user name="aifusr"
sudo="true"
password="$6$WtxZKOyaahvvWQRG$TUys60kQhF0ffBdnDSJVTA.PovwCOajjMz8HEHL2H0ZMi0bFpDTQvKA7BqzM3nA.ZMAUxNjpJP1dG/eA78Zgw0"
comment="A test user for AIF.">
<home path="/opt/aifusr" create="true" />
<xgroup name="admins" create="true" />
<xgroup name="wheel" />
<xgroup name="users" />
<xGroup name="admins" create="true" />
<xGroup name="wheel" />
<xGroup name="users" />
</user>
</users>
<service name="sshd" status="0" />
</system>
<pacman>
<repos>
<repo name="core" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="extra" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="community" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="multilib" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="testing" enabled="false" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="multilib-testing" enabled="false" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="archlinuxfr" enabled="false" siglevel="Optional TrustedOnly" mirror="http://repo.archlinux.fr/$arch" />
<repo name="core" enabled="true" sigLevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="extra" enabled="true" sigLevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="community" enabled="true" sigLevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="multilib" enabled="true" sigLevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="testing" enabled="false" sigLevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="multilib-testing" enabled="false" sigLevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
<repo name="archlinuxfr" enabled="false" sigLevel="Optional TrustedOnly" mirror="http://repo.archlinux.fr/$arch" />
</repos>
<mirrorlist>
<mirror>http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch</mirror>
<mirror>http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch</mirror>
<mirrorList>
<mirror>http://arch.mirror.square-r00t.net/$repo/os/$arch</mirror>
<mirror>http://mirror.us.leaseweb.net/archlinux/$repo/os/$arch</mirror>
<mirror>http://ftp.osuosl.org/pub/archlinux/$repo/os/$arch</mirror>
<mirror>http://arch.mirrors.ionfish.org/$repo/os/$arch</mirror>
<mirror>http://mirrors.gigenet.com/archlinux/$repo/os/$arch</mirror>
<mirror>http://mirror.jmu.edu/pub/archlinux/$repo/os/$arch</mirror>
</mirrorlist>
</mirrorList>
<software>
<package name="sed" repo="core" />
<package name="python" />

View File

@ -31,5 +31,6 @@ setuptools.setup(
project_urls = {'Documentation': 'https://aif-ng.io/',
'Source': 'https://git.square-r00t.net/AIF-NG/',
'Tracker': 'https://bugs.square-r00t.net/index.php?project=9'},
install_requires = ['gpg', 'requests', 'lxml', 'psutil', 'pyparted', 'pytz', 'passlib', 'validators']
install_requires = ['blkinfo', 'gpg', 'lxml', 'passlib', 'psutil',
'pyparted', 'pytz', 'requests', 'validators']
)