Retire astara repo
Retire repository, following https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project Change-Id: I0a8240c961955447d65aee7af24e03cb81da12d3
This commit is contained in:
parent
a6cd8a5594
commit
d139d81213
18
.gitignore
vendored
18
.gitignore
vendored
@ -1,18 +0,0 @@
|
|||||||
.coverage
|
|
||||||
*.py[co]
|
|
||||||
*.sw[po]
|
|
||||||
*.pyc
|
|
||||||
*.egg-info
|
|
||||||
.venv
|
|
||||||
dropin.cache
|
|
||||||
.tox
|
|
||||||
build
|
|
||||||
dist
|
|
||||||
fpm.log*
|
|
||||||
packages/
|
|
||||||
.DS_Store
|
|
||||||
._.DS_Store
|
|
||||||
|
|
||||||
# pbr output
|
|
||||||
AUTHORS
|
|
||||||
ChangeLog
|
|
10
.travis.yml
10
.travis.yml
@ -1,10 +0,0 @@
|
|||||||
language: python
|
|
||||||
python:
|
|
||||||
- "2.7"
|
|
||||||
install:
|
|
||||||
- pip install -r test_requirements.txt --use-mirror
|
|
||||||
- pip install flake8 --use-mirrors
|
|
||||||
- pip install -q . --use-mirrors
|
|
||||||
before_script:
|
|
||||||
- flake8 setup.py akanda
|
|
||||||
script: nosetests -d
|
|
175
LICENSE
175
LICENSE
@ -1,175 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
10
README.md
10
README.md
@ -1,10 +0,0 @@
|
|||||||
# Astara Appliance
|
|
||||||
|
|
||||||
*Part of the [Astara Project](https://github.com/openstack/astara).*
|
|
||||||
|
|
||||||
A Linux-based L3 software router. Includes a REST API to monitor, configure,
|
|
||||||
and manage the router.
|
|
||||||
|
|
||||||
Astara routers are recommended to run with 512 MB of RAM and a single vCPU, and
|
|
||||||
are intended to run within an virtualized L2 overlay to provide complete network
|
|
||||||
virtualization.
|
|
10
README.rst
Normal file
10
README.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
This project is no longer maintained.
|
||||||
|
|
||||||
|
The contents of this repository are still available in the Git
|
||||||
|
source code management system. To see the contents of this
|
||||||
|
repository before it reached its end of life, please check out the
|
||||||
|
previous commit with "git checkout HEAD^1".
|
||||||
|
|
||||||
|
For any further questions, please email
|
||||||
|
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||||
|
Freenode.
|
@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
- hosts: all
|
|
||||||
sudo: True
|
|
||||||
|
|
||||||
vars:
|
|
||||||
bird_enable: False
|
|
||||||
bird6_enable: True
|
|
||||||
bird_enable_service: True
|
|
||||||
strongswan_enable: True
|
|
||||||
strongswan_enable_service: False
|
|
||||||
dnsmasq_conf_dir: /etc/dnsmasq.d
|
|
||||||
dnsmasq_conf_file: /etc/dnsmasq.conf
|
|
||||||
install_extras: False
|
|
||||||
do_cleanup: True
|
|
||||||
router_appliance: True
|
|
||||||
update_kernel: True
|
|
||||||
enabled_advanced_services: "router"
|
|
||||||
tasks:
|
|
||||||
- include: tasks/debian_backports.yml
|
|
||||||
when: ansible_distribution == "Debian" and ansible_distribution_release == "wheezy"
|
|
||||||
- include: tasks/update_kernel.yml
|
|
||||||
when: update_kernel and ansible_distribution_release == "wheezy"
|
|
||||||
- include: tasks/base.yml
|
|
||||||
- include: tasks/astara.yml
|
|
||||||
- include: tasks/bird.yml
|
|
||||||
- include: tasks/conntrackd.yml
|
|
||||||
- include: tasks/strongswan.yml
|
|
||||||
- include: tasks/dnsmasq.yml
|
|
||||||
- include: tasks/extras.yml
|
|
||||||
when: install_extras
|
|
@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- name: install base packages
|
|
||||||
apt: name={{item}} state=installed install_recommends=no
|
|
||||||
with_items:
|
|
||||||
- gunicorn
|
|
||||||
- python-pip
|
|
||||||
- python-dev
|
|
||||||
- logrotate
|
|
||||||
|
|
||||||
- name: copy astara-appliance code
|
|
||||||
synchronize: src={{ playbook_dir }}/.. dest=/tmp/astara-appliance
|
|
||||||
|
|
||||||
- name: ensure latest setuptools
|
|
||||||
pip: name=setuptools state=latest
|
|
||||||
|
|
||||||
- name: install required files
|
|
||||||
pip: requirements=/tmp/astara-appliance/requirements.txt
|
|
||||||
|
|
||||||
- name: install astara-appliance
|
|
||||||
command: python setup.py install chdir=/tmp/astara-appliance
|
|
||||||
|
|
||||||
- name: install astara gunicorn logging directory
|
|
||||||
file: path=/var/log/astara state=directory
|
|
||||||
|
|
||||||
- name: install astara logrotate config
|
|
||||||
template: src=logrotate.j2 dest=/etc/logrotate.d/astara
|
|
||||||
|
|
||||||
- name: install gunicorn config file
|
|
||||||
template: src=gunicorn.j2 dest=/etc/astara_gunicorn_config.py
|
|
||||||
|
|
||||||
|
|
||||||
- name: add gunicorn user
|
|
||||||
action: user name=gunicorn state=present
|
|
||||||
|
|
||||||
- name: install init.d files
|
|
||||||
copy: src={{playbook_dir}}/../scripts/etc/init.d/{{item}} dest=/etc/init.d/{{item}} mode=0555
|
|
||||||
with_items:
|
|
||||||
- metadata
|
|
||||||
- astara-router-api-server
|
|
||||||
|
|
||||||
- name: install rootwrap config file
|
|
||||||
copy: src={{playbook_dir}}/../etc/rootwrap.conf dest=/etc/rootwrap.conf mode=0555
|
|
||||||
|
|
||||||
- name: create /etc/rootwrap.d/
|
|
||||||
file: path=/etc/rootwrap.d/ state=directory
|
|
||||||
|
|
||||||
- name: install rootwrap rules file
|
|
||||||
copy: src={{playbook_dir}}/../etc/rootwrap.d/ dest=/etc/rootwrap.d/ mode=0555
|
|
||||||
with_items:
|
|
||||||
- network.filters
|
|
||||||
|
|
||||||
- stat: path=/etc/sudoers.d
|
|
||||||
register: sudoers_dir
|
|
||||||
|
|
||||||
- name: install sudoer file
|
|
||||||
when: sudoers_dir.stat.exists == True
|
|
||||||
template: src=gunicorn dest=/etc/sudoers.d/gunicorn mode=0440
|
|
||||||
|
|
||||||
- name: create /usr/local/share/astara/
|
|
||||||
file: path=/usr/local/share/astara state=directory
|
|
||||||
|
|
||||||
- name: make /usr/local/share/astara/ importable
|
|
||||||
copy: dest=/usr/local/share/astara/__init__.py content=''
|
|
||||||
|
|
||||||
- name: install astara_local_settings.py
|
|
||||||
copy: dest=/usr/local/share/astara/astara_local_settings.py content='ENABLED_SERVICES = {{enabled_advanced_services.split(',')}}\n'
|
|
||||||
|
|
||||||
- name: update-rc
|
|
||||||
command: update-rc.d astara-router-api-server start
|
|
||||||
|
|
||||||
- name: add timestamp
|
|
||||||
shell: date > arg1 creates=/etc/astara-release
|
|
||||||
|
|
||||||
- name: enable forwarding
|
|
||||||
sysctl: name={{item}} value=1 sysctl_set=yes state=present reload=yes
|
|
||||||
with_items:
|
|
||||||
- net.ipv4.ip_forward
|
|
||||||
- net.ipv6.conf.all.forwarding
|
|
||||||
when: router_appliance
|
|
||||||
|
|
||||||
- name: remove packages only needed for build
|
|
||||||
apt: name={{item}} state=absent
|
|
||||||
with_items:
|
|
||||||
- python-pip
|
|
||||||
- python-dev
|
|
||||||
- build-essential
|
|
||||||
when: do_cleanup
|
|
||||||
|
|
||||||
- name: Autoremove unused packages
|
|
||||||
command: apt-get -y autoremove
|
|
||||||
when: do_cleanup
|
|
||||||
|
|
||||||
- name: Ensure gunicorn is restarted
|
|
||||||
service: name=astara-router-api-server state=restarted enabled=yes
|
|
||||||
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- name: install base packages
|
|
||||||
apt: name={{item}} state=installed install_recommends=no
|
|
||||||
with_items:
|
|
||||||
- wget
|
|
||||||
- iptables
|
|
||||||
- iptables-persistent
|
|
||||||
- iputils-ping
|
|
||||||
- conntrack
|
|
||||||
- ntp
|
|
||||||
- tcpdump
|
|
||||||
- vim
|
|
||||||
- keepalived
|
|
||||||
- conntrackd
|
|
||||||
|
|
||||||
- name: latest bash (CVE-2014-6271)
|
|
||||||
apt: name=bash state=latest install_recommends=no
|
|
||||||
|
|
||||||
- name: remove timezone
|
|
||||||
command: rm -f arg1 removes=/etc/localtime
|
|
||||||
|
|
||||||
- name: set timezone to UTC
|
|
||||||
command: ln -s /usr/share/zoneinfo/UTC arg1 creates=/etc/localtime
|
|
||||||
|
|
||||||
- name: setting hostname
|
|
||||||
copy: content="astara-linux" dest=/etc/hostname
|
|
||||||
|
|
||||||
- name: set default nameserver
|
|
||||||
copy: content="nameserver 8.8.8.8" dest=/etc/resolv.conf
|
|
||||||
|
|
||||||
- name: vanity motd
|
|
||||||
template: src=motd.j2 dest=/etc/motd
|
|
||||||
|
|
||||||
- name: disable fsck on boot via fastboot
|
|
||||||
file: path=/fastboot state=touch
|
|
||||||
|
|
||||||
- name: reset v4 persistent table rules
|
|
||||||
template: src=rules_v4.j2 dest=/etc/iptables/rules.v4
|
|
||||||
|
|
||||||
- name: reset v6 persistent table rules
|
|
||||||
template: src=rules_v6.j2 dest=/etc/iptables/rules.v6
|
|
||||||
|
|
||||||
- name: clear out network interfaces.d
|
|
||||||
shell: rm -f /etc/network/interfaces.d/*
|
|
||||||
|
|
||||||
- name: reset network interfaces
|
|
||||||
file: content="auto lo\niface lo inet loopback" dest=/etc/network/interfaces
|
|
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- name: install bird
|
|
||||||
apt: name=bird state=installed install_recommends=no
|
|
||||||
when: bird_enable
|
|
||||||
|
|
||||||
- name: install bird6
|
|
||||||
apt: name=bird6 state=installed install_recommends=no
|
|
||||||
when: bird6_enable
|
|
||||||
|
|
||||||
# Debian version does not support status ensure that it exists
|
|
||||||
- name: ensure bird status works in init.d
|
|
||||||
replace: dest=/etc/init.d/bird regexp='(\;\;\s*)\n(\s*reload\|)' replace='\1\n status)\n status_of_proc $DAEMON $NAME && exit 0 || exit $?\n ;;\n\2'
|
|
||||||
when: bird_enable
|
|
||||||
|
|
||||||
- name: ensure bird6 status works in init.d
|
|
||||||
replace: dest=/etc/init.d/bird6 regexp='(\;\;\s*)\n(\s*reload\|)' replace='\1\n status)\n status_of_proc $DAEMON $NAME && exit 0 || exit $?\n ;;\n\2'
|
|
||||||
when: bird6_enable
|
|
||||||
|
|
||||||
- name: Ensure bird is started
|
|
||||||
service: name=bird state=started enabled=yes
|
|
||||||
when: bird_enable and bird_enable_service
|
|
||||||
|
|
||||||
- name: Ensure bird6 is started
|
|
||||||
service: name=bird6 state=started enabled=yes
|
|
||||||
when: bird6_enable and bird_enable_service
|
|
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- name: install conntrackd
|
|
||||||
apt: name=conntrackd state=installed install_recommends=no
|
|
||||||
|
|
||||||
- name: install conntrackd notify script to /etc/conntrackd
|
|
||||||
copy: src=/usr/share/doc/conntrackd/examples/sync/primary-backup.sh dest=/etc/conntrackd/primary-backup.sh mode=0755
|
|
@ -1,5 +0,0 @@
|
|||||||
- name: Install Wheezy Backports and update
|
|
||||||
apt_repository: repo="deb http://http.debian.net/debian wheezy-backports main"
|
|
||||||
|
|
||||||
- name: Update Cache
|
|
||||||
apt: update_cache=yes cache_valid_time=3600
|
|
@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
- name: install dnsmasq (Debian)
|
|
||||||
apt: name=dnsmasq state=installed install_recommends=no
|
|
||||||
|
|
||||||
- name: Create config directory
|
|
||||||
file: path={{dnsmasq_conf_dir}} state=directory mode=0755
|
|
||||||
|
|
||||||
- name: Generate Config
|
|
||||||
template: src=dnsmasq.conf.j2 dest={{dnsmasq_conf_file}}
|
|
||||||
|
|
||||||
- name: Ensure dnsmasq is started
|
|
||||||
service: name=dnsmasq state=started enabled=yes
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- name: install extras
|
|
||||||
apt: name={{item}} state=installed install_recommends=no
|
|
||||||
with_items:
|
|
||||||
- mtr
|
|
||||||
- tshark
|
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- name: install strongswan
|
|
||||||
apt: name=strongswan state=installed install_recommends=yes
|
|
||||||
when: strongswan_enable
|
|
||||||
|
|
||||||
- name: Ensure strongswan is started
|
|
||||||
service: name=strongswan state=started enabled=yes
|
|
||||||
when: strongswan_enable and strongswan_enable_service
|
|
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- stat: path=/boot/grub
|
|
||||||
register: grub_dir
|
|
||||||
|
|
||||||
- stat: path=/boot
|
|
||||||
register: boot_dir
|
|
||||||
|
|
||||||
- name: install kernel (Debian)
|
|
||||||
apt: name=linux-image-amd64 state=latest install_recommends=no default_release=wheezy-backports
|
|
||||||
|
|
||||||
- name: update grub conf
|
|
||||||
when: grub_dir.stat.exists == True
|
|
||||||
template: src=default_grub dest=/etc/default/grub
|
|
||||||
|
|
||||||
- stat: path=/boot
|
|
||||||
register: boot_dir_after
|
|
||||||
|
|
||||||
- name: update-grub
|
|
||||||
when: boot_dir_after.stat.mtime > boot_dir.stat.mtime and grub_dir.stat.exists == True
|
|
||||||
command: update-grub
|
|
@ -1,9 +0,0 @@
|
|||||||
# If you change this file, run 'update-grub' afterwards to update
|
|
||||||
# /boot/grub/grub.cfg.
|
|
||||||
|
|
||||||
GRUB_DEFAULT=0
|
|
||||||
GRUB_TIMEOUT=0
|
|
||||||
GRUB_DISTRIBUTOR=Debian
|
|
||||||
GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0,115200n8"
|
|
||||||
# Disable GSO (Generic Segmentation Offload) in order to improve IPv6 forwarding performance
|
|
||||||
GRUB_CMDLINE_LINUX="debian-installer=en_US virtio_net.gso=0"
|
|
@ -1,9 +0,0 @@
|
|||||||
bind-interfaces
|
|
||||||
leasefile-ro
|
|
||||||
domain-needed
|
|
||||||
bogus-priv
|
|
||||||
no-hosts
|
|
||||||
no-poll
|
|
||||||
strict-order
|
|
||||||
dhcp-lease-max=256
|
|
||||||
conf-dir={{dnsmasq_conf_dir}}
|
|
@ -1 +0,0 @@
|
|||||||
gunicorn ALL = (root) NOPASSWD: /usr/local/bin/astara-rootwrap /etc/rootwrap.conf *
|
|
@ -1,12 +0,0 @@
|
|||||||
import multiprocessing
|
|
||||||
|
|
||||||
bind = '[::]:5000'
|
|
||||||
workers = workers = multiprocessing.cpu_count() * 2 + 1
|
|
||||||
timeout = 300
|
|
||||||
backlog = 2048
|
|
||||||
worker_class ="sync"
|
|
||||||
debug = False
|
|
||||||
daemon = True
|
|
||||||
pidfile = "/var/run/gunicorn.pid"
|
|
||||||
errorlog = "/var/log/astara/gunicorn_error.log"
|
|
||||||
accesslog = "/var/log/astara/gunicorn_access.log"
|
|
@ -1,10 +0,0 @@
|
|||||||
/var/log/astara/*.log {
|
|
||||||
weekly
|
|
||||||
rotate 7
|
|
||||||
compress
|
|
||||||
missingok
|
|
||||||
create 644 root root
|
|
||||||
postrotate
|
|
||||||
kill -USR1 `cat /var/run/gunicorn.pid`
|
|
||||||
endscript
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
___ _
|
|
||||||
/ _ \ | | L3 for OpenStack
|
|
||||||
/ /_\ \___| |_ __ _ _ __ __ _
|
|
||||||
| _ / __| __/ _` | '__/ _` |
|
|
||||||
| | | \__ \ || (_| | | | (_| |
|
|
||||||
\_| |_/___/\__\__,_|_| \__,_|
|
|
||||||
|
|
||||||
Welcome to Astara: Powered by Unicorns.
|
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by iptables-save v1.4.14 on Sun Apr 12 20:07:15 2015
|
|
||||||
*filter
|
|
||||||
:INPUT ACCEPT [0:0]
|
|
||||||
:FORWARD ACCEPT [0:0]
|
|
||||||
:OUTPUT ACCEPT [0:0]
|
|
||||||
COMMIT
|
|
||||||
# Completed on Sun Apr 12 20:07:15 2015
|
|
||||||
# Generated by iptables-save v1.4.14 on Sun Apr 12 20:07:15 2015
|
|
||||||
*mangle
|
|
||||||
:PREROUTING ACCEPT [0:0]
|
|
||||||
:INPUT ACCEPT [0:0]
|
|
||||||
:FORWARD ACCEPT [0:0]
|
|
||||||
:OUTPUT ACCEPT [0:0]
|
|
||||||
:POSTROUTING ACCEPT [0:0]
|
|
||||||
COMMIT
|
|
||||||
# Completed on Sun Apr 12 20:07:15 2015
|
|
||||||
# Generated by iptables-save v1.4.14 on Sun Apr 12 20:07:15 2015
|
|
||||||
*nat
|
|
||||||
:PREROUTING ACCEPT [0:0]
|
|
||||||
:INPUT ACCEPT [0:0]
|
|
||||||
:OUTPUT ACCEPT [0:0]
|
|
||||||
:POSTROUTING ACCEPT [0:0]
|
|
||||||
COMMIT
|
|
||||||
# Completed on Sun Apr 12 20:07:15 2015
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
# Generated by ip6tables-save v1.4.14 on Sun Apr 12 20:14:31 2015
|
|
||||||
*filter
|
|
||||||
:INPUT ACCEPT [0:0]
|
|
||||||
:FORWARD ACCEPT [0:0]
|
|
||||||
:OUTPUT ACCEPT [0:0]
|
|
||||||
COMMIT
|
|
||||||
# Completed on Sun Apr 12 20:14:31 2015
|
|
@ -1,15 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
@ -1,15 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
@ -1,49 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
"""Set up the API server application instance
|
|
||||||
"""
|
|
||||||
import flask
|
|
||||||
|
|
||||||
from astara_router.api import v1
|
|
||||||
from astara_router.debug import handle_traceback
|
|
||||||
from astara_router.manager import manager
|
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
|
||||||
app.register_blueprint(v1.base.blueprint)
|
|
||||||
app.register_blueprint(v1.system.blueprint)
|
|
||||||
app.register_blueprint(v1.firewall.blueprint)
|
|
||||||
app.register_blueprint(v1.status.blueprint)
|
|
||||||
app.register_error_handler(500, handle_traceback)
|
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def attach_config():
|
|
||||||
'''
|
|
||||||
Attach any configuration before instantiating API
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# TODO(mark): make this use a config file ie
|
|
||||||
# app.config.from_object('astara_router.config.Default')
|
|
||||||
# manager.state_path = app.config['STATE_PATH']
|
|
||||||
|
|
||||||
addr = str(manager.ip_mgr.get_interfaces()[0].addresses[0])
|
|
||||||
app.run(host=addr,
|
|
||||||
port=5000)
|
|
@ -1,22 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
# flake8: noqa
|
|
||||||
import base
|
|
||||||
import firewall
|
|
||||||
import system
|
|
||||||
import status
|
|
@ -1,32 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Blueprint for the "base" portion of the version 1 of the API.
|
|
||||||
"""
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
blueprint = utils.blueprint_factory(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/')
|
|
||||||
def welcome():
|
|
||||||
'''
|
|
||||||
Show welcome message
|
|
||||||
'''
|
|
||||||
return 'Astara appliance API service is active'
|
|
@ -1,41 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Blueprint for version 1 of the firewall API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import request
|
|
||||||
|
|
||||||
from astara_router import utils
|
|
||||||
from astara_router.drivers import iptables
|
|
||||||
|
|
||||||
|
|
||||||
blueprint = utils.blueprint_factory(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.before_request
|
|
||||||
def get_manager():
|
|
||||||
request.iptables_mgr = iptables.IPTablesManager()
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/rules')
|
|
||||||
def get_rules():
|
|
||||||
'''
|
|
||||||
Show loaded firewall rules by iptables
|
|
||||||
'''
|
|
||||||
return request.iptables_mgr.get_rules()
|
|
@ -1,43 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Blueprint for the "status" portion of the version 1 of the API.
|
|
||||||
"""
|
|
||||||
from flask import request
|
|
||||||
|
|
||||||
from astara_router import utils
|
|
||||||
from astara_router.drivers import ping
|
|
||||||
|
|
||||||
blueprint = utils.blueprint_factory(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.before_request
|
|
||||||
def get_manager():
|
|
||||||
request.ping_mgr = ping.PingManager()
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/')
|
|
||||||
@utils.json_response
|
|
||||||
def status():
|
|
||||||
""" Return router healt status """
|
|
||||||
|
|
||||||
retval = {}
|
|
||||||
# Attempt to reach public Google DNS as an ext network test
|
|
||||||
retval['v4'] = request.ping_mgr.do('8.8.8.8')
|
|
||||||
retval['v6'] = request.ping_mgr.do('2001:4860:4860::8888')
|
|
||||||
return retval
|
|
@ -1,155 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Blueprint for the "system" portion of the version 1 of the API.
|
|
||||||
"""
|
|
||||||
from flask import Response
|
|
||||||
from flask import abort, request
|
|
||||||
from dogpile.cache import make_region
|
|
||||||
|
|
||||||
from astara_router import models
|
|
||||||
from astara_router import utils
|
|
||||||
from astara_router import settings
|
|
||||||
from astara_router.manager import manager
|
|
||||||
|
|
||||||
blueprint = utils.blueprint_factory(__name__)
|
|
||||||
|
|
||||||
# Managed by _get_cache()
|
|
||||||
_cache = None
|
|
||||||
|
|
||||||
|
|
||||||
ADVANCED_SERVICES_KEY = 'services'
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cache():
|
|
||||||
global _cache
|
|
||||||
if _cache is None:
|
|
||||||
_cache = make_region().configure(
|
|
||||||
'dogpile.cache.dbm',
|
|
||||||
arguments={
|
|
||||||
"filename": "/tmp/astara-state"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return _cache
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/interface/<ifname>')
|
|
||||||
@utils.json_response
|
|
||||||
def get_interface(ifname):
|
|
||||||
'''
|
|
||||||
Show interface parameters given an interface name.
|
|
||||||
For example ge1, ge2 for generic ethernet
|
|
||||||
'''
|
|
||||||
return dict(interface=manager.router.get_interface(ifname))
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/interfaces')
|
|
||||||
@utils.json_response
|
|
||||||
def get_interfaces():
|
|
||||||
'''
|
|
||||||
Show all interfaces and parameters
|
|
||||||
'''
|
|
||||||
return dict(interfaces=manager.router.get_interfaces())
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/config', methods=['GET'])
|
|
||||||
@utils.json_response
|
|
||||||
def get_configuration():
|
|
||||||
"""Return the current router configuration."""
|
|
||||||
return dict(configuration=manager.config)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/config', methods=['PUT'])
|
|
||||||
@utils.json_response
|
|
||||||
def put_configuration():
|
|
||||||
if request.content_type != 'application/json':
|
|
||||||
abort(415)
|
|
||||||
|
|
||||||
try:
|
|
||||||
system_config_candidate = models.SystemConfiguration(request.json)
|
|
||||||
except ValueError, e:
|
|
||||||
return Response(
|
|
||||||
'The system config failed to deserialize.\n' + str(e),
|
|
||||||
status=422)
|
|
||||||
|
|
||||||
errors = system_config_candidate.validate()
|
|
||||||
if errors:
|
|
||||||
return Response(
|
|
||||||
'The config failed to validate.\n' + '\n'.join(errors),
|
|
||||||
status=422)
|
|
||||||
|
|
||||||
# Config requests to a router appliance will always contain a default ASN,
|
|
||||||
# so we can key on that for now. Later on we need to move router stuff
|
|
||||||
# to the extensible list of things the appliance can handle
|
|
||||||
if request.json.get('asn'):
|
|
||||||
try:
|
|
||||||
router_config_candidate = models.RouterConfiguration(request.json)
|
|
||||||
except ValueError, e:
|
|
||||||
return Response(
|
|
||||||
'The router config failed to deserialize.\n' + str(e),
|
|
||||||
status=422)
|
|
||||||
|
|
||||||
errors = router_config_candidate.validate()
|
|
||||||
if errors:
|
|
||||||
return Response(
|
|
||||||
'The config failed to validate.\n' + '\n'.join(errors),
|
|
||||||
status=422)
|
|
||||||
else:
|
|
||||||
router_config_candidate = None
|
|
||||||
|
|
||||||
if router_config_candidate:
|
|
||||||
advanced_service_configs = [router_config_candidate]
|
|
||||||
else:
|
|
||||||
advanced_service_configs = []
|
|
||||||
|
|
||||||
advanced_services = request.json.get(ADVANCED_SERVICES_KEY, {})
|
|
||||||
for svc in advanced_services.keys():
|
|
||||||
if svc not in settings.ENABLED_SERVICES:
|
|
||||||
return Response(
|
|
||||||
'This appliance cannot service requested advanced '
|
|
||||||
'service: %s' % svc, status=400)
|
|
||||||
|
|
||||||
for svc in settings.ENABLED_SERVICES:
|
|
||||||
if not advanced_services.get(svc):
|
|
||||||
continue
|
|
||||||
|
|
||||||
config_model = models.get_config_model(service=svc)
|
|
||||||
if not config_model:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
svc_config_candidate = config_model(advanced_services.get(svc))
|
|
||||||
except ValueError, e:
|
|
||||||
return Response(
|
|
||||||
'The %s config failed to deserialize.\n' + str(e) %
|
|
||||||
config_model.service_name, status=422)
|
|
||||||
|
|
||||||
errors = svc_config_candidate.validate()
|
|
||||||
if errors:
|
|
||||||
return Response(
|
|
||||||
'The %s config failed to validate.\n' + '\n'.join(errors),
|
|
||||||
config_model.service_name, status=422)
|
|
||||||
|
|
||||||
advanced_service_configs.append(svc_config_candidate)
|
|
||||||
|
|
||||||
manager.update_config(
|
|
||||||
system_config=system_config_candidate,
|
|
||||||
service_configs=advanced_service_configs,
|
|
||||||
cache=_get_cache())
|
|
||||||
|
|
||||||
return dict(configuration=manager.config)
|
|
@ -1,15 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
@ -1,102 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
# Copyright 2015 Akanda, Inc
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import netaddr
|
|
||||||
|
|
||||||
from astara_router import settings
|
|
||||||
from astara_router import utils
|
|
||||||
from astara_router.drivers import ip
|
|
||||||
|
|
||||||
|
|
||||||
def configure_ssh(listen_ip):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
config = open('/etc/ssh/sshd_config', 'r').read()
|
|
||||||
config = re.sub(
|
|
||||||
'(^|\n)(#)?(ListenAddress|AddressFamily|UseDNS) .*',
|
|
||||||
'',
|
|
||||||
config
|
|
||||||
)
|
|
||||||
|
|
||||||
config += '\n'.join([
|
|
||||||
'', # make sure we have a blank line at the end before adding more
|
|
||||||
'AddressFamily inet%s' % ('6' if listen_ip.version == 6 else ''),
|
|
||||||
'ListenAddress ' + str(listen_ip),
|
|
||||||
'UseDNS no'
|
|
||||||
])
|
|
||||||
try:
|
|
||||||
open('/etc/ssh/sshd_config', 'w+').write(config)
|
|
||||||
sys.stderr.write('sshd configured to listen on %s\n' % listen_ip)
|
|
||||||
except:
|
|
||||||
sys.stderr.write('Unable to write sshd configuration file.')
|
|
||||||
|
|
||||||
|
|
||||||
def configure_gunicorn(listen_ip):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
if listen_ip.version == 6:
|
|
||||||
bind = "'[%s]:%d'" % (listen_ip, settings.API_SERVICE)
|
|
||||||
else:
|
|
||||||
bind = "'%s:%d'" % (listen_ip, settings.API_SERVICE)
|
|
||||||
|
|
||||||
config = open('/etc/astara_gunicorn_config.py', 'r').read()
|
|
||||||
config = re.sub('\nbind(\s)?\=(\s)?.*', '\nbind = %s' % bind, config)
|
|
||||||
|
|
||||||
try:
|
|
||||||
open('/etc/astara_gunicorn_config.py', 'w+').write(config)
|
|
||||||
sys.stderr.write('http configured to listen on %s\n' % listen_ip)
|
|
||||||
except:
|
|
||||||
sys.stderr.write('Unable to write gunicorn configuration file.')
|
|
||||||
|
|
||||||
|
|
||||||
def configure_management():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Configure Management Interface'
|
|
||||||
)
|
|
||||||
parser.add_argument('mac_address', metavar='lladdr', type=str)
|
|
||||||
parser.add_argument('ip_address', metavar='ipaddr', type=str)
|
|
||||||
parser.add_argument('--mtu', metavar='mtu', type=int, default=1280)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
ip_addr = netaddr.IPNetwork(args.ip_address)
|
|
||||||
|
|
||||||
mgr = ip.IPManager()
|
|
||||||
|
|
||||||
for intf in mgr.get_interfaces():
|
|
||||||
if args.mac_address == intf.lladdr:
|
|
||||||
if not intf.is_up:
|
|
||||||
mgr.up(intf)
|
|
||||||
|
|
||||||
intf.mtu = args.mtu
|
|
||||||
|
|
||||||
if ip_addr not in intf.addresses:
|
|
||||||
if ip_addr.version == 6:
|
|
||||||
real_ifname = mgr.generic_to_host(intf.ifname)
|
|
||||||
utils.execute([
|
|
||||||
'sysctl',
|
|
||||||
'-w',
|
|
||||||
'net.ipv6.conf.%s.accept_dad=0' % real_ifname
|
|
||||||
])
|
|
||||||
|
|
||||||
intf.addresses.append(ip_addr)
|
|
||||||
mgr.update_interface(intf)
|
|
||||||
configure_ssh(ip_addr.ip)
|
|
||||||
configure_gunicorn(ip_addr.ip)
|
|
||||||
break
|
|
@ -1,34 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from traceback import print_exc
|
|
||||||
from pprint import pformat
|
|
||||||
import cStringIO
|
|
||||||
|
|
||||||
from flask import request
|
|
||||||
from werkzeug import Response
|
|
||||||
|
|
||||||
|
|
||||||
def handle_traceback(exc):
|
|
||||||
out = cStringIO.StringIO()
|
|
||||||
print_exc(file=out)
|
|
||||||
formatted_environ = pformat(request.environ)
|
|
||||||
response = Response(
|
|
||||||
'%s\n%s\n' % (out.getvalue(), formatted_environ),
|
|
||||||
status=500
|
|
||||||
)
|
|
||||||
return response
|
|
@ -1,57 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
SSH = 22
|
|
||||||
SMTP = 25
|
|
||||||
DNS = 53
|
|
||||||
HTTP = 80
|
|
||||||
BGP = 179
|
|
||||||
HTTPS = 443
|
|
||||||
HTTP_ALT = 8080
|
|
||||||
API_SERVICE = 5000
|
|
||||||
|
|
||||||
DHCP = 67
|
|
||||||
DHCPV6 = 546
|
|
||||||
|
|
||||||
ISAKMP = 500
|
|
||||||
IPSEC_NAT_T = 4500
|
|
||||||
|
|
||||||
NFS_DEVELOPMENT = [111, 1110, 2049, 4045]
|
|
||||||
|
|
||||||
MANAGEMENT_PORTS = [SSH, API_SERVICE] # + NFS_DEVELOPMENT
|
|
||||||
|
|
||||||
# destination address for AWS compliant metadata guests
|
|
||||||
METADATA_DEST_ADDRESS = '169.254.169.254'
|
|
||||||
|
|
||||||
# port for internal network metadata proxy
|
|
||||||
BASE_METADATA_PORT = 9600
|
|
||||||
|
|
||||||
# default address of orchestrator metadata service
|
|
||||||
ORCHESTRATOR_METADATA_ADDRESS = 'fdca:3ba5:a17a:acda::1'
|
|
||||||
|
|
||||||
# default port for orchestrator metadata service
|
|
||||||
ORCHESTRATOR_METADATA_PORT = 9697
|
|
||||||
|
|
||||||
|
|
||||||
def internal_metadata_port(ifname):
|
|
||||||
return BASE_METADATA_PORT + int(re.sub('[a-zA-Z]', '', ifname))
|
|
||||||
|
|
||||||
# Configures which advanced service drivers are loaded by this
|
|
||||||
# instance of the appliance.
|
|
||||||
ENABLED_SERVICES = ['router']
|
|
@ -1,15 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
@ -1,159 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from astara_router import utils
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router.models import Network
|
|
||||||
|
|
||||||
|
|
||||||
def send_gratuitous_arp():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Send a gratuitous ARP'
|
|
||||||
)
|
|
||||||
parser.add_argument('ifname', metavar='ifname', type=str)
|
|
||||||
parser.add_argument('address', metavar='address', type=str)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
return _send_gratuitous_arp(args.ifname, args.address)
|
|
||||||
|
|
||||||
|
|
||||||
def _send_gratuitous_arp(ifname, address):
|
|
||||||
"""
|
|
||||||
Send a gratuitous ARP reply. Generally used when Floating IPs are
|
|
||||||
associated.
|
|
||||||
:type ifname: str
|
|
||||||
:param ifname: The real name of the interface to send an ARP on
|
|
||||||
:type address: str
|
|
||||||
:param address: The source IPv4 address
|
|
||||||
"""
|
|
||||||
HTYPE_ARP = 0x0806
|
|
||||||
PTYPE_IPV4 = 0x0800
|
|
||||||
|
|
||||||
# Bind to the socket
|
|
||||||
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
|
|
||||||
sock.bind((ifname, HTYPE_ARP))
|
|
||||||
hwaddr = sock.getsockname()[4]
|
|
||||||
|
|
||||||
# Build a gratuitous ARP packet
|
|
||||||
gratuitous_arp = [
|
|
||||||
struct.pack("!h", 1), # HTYPE Ethernet
|
|
||||||
struct.pack("!h", PTYPE_IPV4), # PTYPE IPv4
|
|
||||||
struct.pack("!B", 6), # HADDR length, 6 for IEEE 802 MAC addresses
|
|
||||||
struct.pack("!B", 4), # PADDR length, 4 for IPv4
|
|
||||||
struct.pack("!h", 2), # OPER, 2 = ARP Reply
|
|
||||||
|
|
||||||
# Sender's hardware and protocol address are duplicated in the
|
|
||||||
# target fields
|
|
||||||
|
|
||||||
hwaddr, # Sender MAC
|
|
||||||
socket.inet_aton(address), # Sender IP address
|
|
||||||
hwaddr, # Target MAC
|
|
||||||
socket.inet_aton(address) # Target IP address
|
|
||||||
]
|
|
||||||
frame = [
|
|
||||||
'\xff\xff\xff\xff\xff\xff', # Broadcast destination
|
|
||||||
hwaddr, # Source address
|
|
||||||
struct.pack("!h", HTYPE_ARP),
|
|
||||||
''.join(gratuitous_arp)
|
|
||||||
]
|
|
||||||
sock.send(''.join(frame))
|
|
||||||
sock.close()
|
|
||||||
|
|
||||||
|
|
||||||
class ARPManager(base.Manager):
|
|
||||||
"""
|
|
||||||
A class to interact with entries in the ARP cache. Currently only really
|
|
||||||
provides support for deleting stuff from the cache.
|
|
||||||
"""
|
|
||||||
EXECUTABLE = 'arp'
|
|
||||||
|
|
||||||
def send_gratuitous_arp_for_floating_ips(self, config, generic_to_host):
|
|
||||||
"""
|
|
||||||
Send a gratuitous ARP for every Floating IP.
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config: An astara_router.models.Configuration object containing
|
|
||||||
configuration information for the system's network
|
|
||||||
setup.
|
|
||||||
:type generic_to_host: callable
|
|
||||||
:param generic_to_host: A callable which translates a generic interface
|
|
||||||
name (e.g., "ge0") to a physical name (e.g.,
|
|
||||||
"eth0")
|
|
||||||
"""
|
|
||||||
external_nets = filter(
|
|
||||||
lambda n: n.network_type == Network.TYPE_EXTERNAL,
|
|
||||||
config.networks
|
|
||||||
)
|
|
||||||
for net in external_nets:
|
|
||||||
for fip in net.floating_ips:
|
|
||||||
utils.execute([
|
|
||||||
'astara-gratuitous-arp',
|
|
||||||
generic_to_host(net.interface.ifname),
|
|
||||||
str(fip.floating_ip)
|
|
||||||
], self.root_helper)
|
|
||||||
|
|
||||||
def remove_stale_entries(self, config):
|
|
||||||
"""
|
|
||||||
A wrapper function that iterates over the networks in <config> and
|
|
||||||
removes arp entries that no longer have any networks associated with
|
|
||||||
them. This function calls _delete_from_arp_cache to do the actual
|
|
||||||
deletion and makes calls to _mac_address_for_ip to match arp entries
|
|
||||||
to network interface IPs.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config: An astara_router.models.Configuration object containing
|
|
||||||
configuration information for the system's network
|
|
||||||
setup.
|
|
||||||
"""
|
|
||||||
for network in config.networks:
|
|
||||||
for a in network.address_allocations:
|
|
||||||
for ip in a.dhcp_addresses:
|
|
||||||
address_for_ip = self._mac_address_for_ip(ip)
|
|
||||||
if address_for_ip and address_for_ip != a.mac_address:
|
|
||||||
self._delete_from_arp_cache(ip)
|
|
||||||
|
|
||||||
def _mac_address_for_ip(self, ip):
|
|
||||||
"""
|
|
||||||
Matches a network's IP address to an arp entry. This is used to
|
|
||||||
associate arp entries with networks that are configured on the system
|
|
||||||
and to determine which arp entries are stale through process of
|
|
||||||
elemination.
|
|
||||||
|
|
||||||
:type ip: str
|
|
||||||
:param ip: IP address to search for in the ARP table.
|
|
||||||
"""
|
|
||||||
cmd_out = self.sudo('-an')
|
|
||||||
match = re.search(' \(%s\) at ([^\s]+)' % ip, cmd_out)
|
|
||||||
if match and match.groups():
|
|
||||||
return match.group(1)
|
|
||||||
|
|
||||||
def _delete_from_arp_cache(self, ip):
|
|
||||||
"""
|
|
||||||
Runs `arp -d <ip>` to delete <ip> from the arp cache.
|
|
||||||
|
|
||||||
:type ip: str
|
|
||||||
:param ip: IP address to search for in the ARP table.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.sudo('-d', ip)
|
|
||||||
except:
|
|
||||||
# We may be attempting to delete from ARP for interfaces which
|
|
||||||
# are managed by keepalived and do not yet have addresses
|
|
||||||
pass
|
|
@ -1,60 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
class Manager(object):
|
|
||||||
"""
|
|
||||||
A base class that provides access to common functions used in other driver
|
|
||||||
modules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes Manager class. <root_helper> provides a facility to specify
|
|
||||||
how this class accesses escalated privileges. Defaults to 'sudo'.
|
|
||||||
|
|
||||||
:type root_helper: str
|
|
||||||
:param root_helper: The method used to obtain escalated privileges.
|
|
||||||
This command will be passed just before the command
|
|
||||||
passed to self.sudo/do. If root_helper is 'sudo'
|
|
||||||
then this will look like: `sudo ls`.
|
|
||||||
"""
|
|
||||||
self.root_helper = root_helper
|
|
||||||
|
|
||||||
def sudo(self, *args):
|
|
||||||
"""
|
|
||||||
Executes command <args> with the specified flags through the
|
|
||||||
root_helper facility (i.e. escalated privileges).
|
|
||||||
|
|
||||||
:type args: tuple
|
|
||||||
:param args: A command, and flags, to execute.
|
|
||||||
:rtype: tuple
|
|
||||||
"""
|
|
||||||
return utils.execute([self.EXECUTABLE] + list(args), self.root_helper)
|
|
||||||
|
|
||||||
def do(self, *args):
|
|
||||||
"""
|
|
||||||
Executes command <args> with specified flags and without escalated
|
|
||||||
privileges.
|
|
||||||
|
|
||||||
:type args: tuple
|
|
||||||
:param args: A command, and flags, to execute.
|
|
||||||
:rtype: tuple
|
|
||||||
"""
|
|
||||||
return utils.execute([self.EXECUTABLE] + list(args))
|
|
@ -1,333 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import random
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
CONF_PATH = '/etc/bird/bird6.conf'
|
|
||||||
DEFAULT_AREA = 0
|
|
||||||
|
|
||||||
|
|
||||||
class BirdManager(base.Manager):
|
|
||||||
"""
|
|
||||||
A class to interact with BIRD, an internet routing protocol daemon.
|
|
||||||
"""
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes BirdManager class.
|
|
||||||
|
|
||||||
:type root_helper: string
|
|
||||||
:param root_helper: System utility used to gain escalate privileges.
|
|
||||||
"""
|
|
||||||
super(BirdManager, self).__init__(root_helper)
|
|
||||||
|
|
||||||
def save_config(self, config, if_map):
|
|
||||||
"""
|
|
||||||
Writes config file for bird daemon.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:type if_map: dict
|
|
||||||
:param if_map: A (dict) mapping of generic to physical hostname, e.g.:
|
|
||||||
{'ge0': 'eth0', 'ge1': 'eth1'}
|
|
||||||
"""
|
|
||||||
config_data = build_config(config, if_map)
|
|
||||||
|
|
||||||
utils.replace_file('/tmp/bird6.conf', config_data)
|
|
||||||
utils.execute(['mv', '/tmp/bird6.conf', CONF_PATH], self.root_helper)
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""
|
|
||||||
Restart the BIRD daemon using the system provided init scripts.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
utils.execute(['service', 'bird6', 'status'], self.root_helper)
|
|
||||||
except: # pragma no cover
|
|
||||||
utils.execute(['service', 'bird6', 'start'], self.root_helper)
|
|
||||||
else: # pragma no cover
|
|
||||||
utils.execute(['service', 'bird6', 'reload'], self.root_helper)
|
|
||||||
|
|
||||||
|
|
||||||
def build_config(config, interface_map):
|
|
||||||
"""
|
|
||||||
Generate a configuration file for the BIRD daemon with interface mapping
|
|
||||||
provided by <interface_map>.
|
|
||||||
|
|
||||||
:type interface_map: dict
|
|
||||||
:param interface_map: A (dict) mapping of generic to physical hostname:
|
|
||||||
{'ge0': 'eth0', 'ge1': 'eth1'}
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
config_data = [
|
|
||||||
_build_global_config(config),
|
|
||||||
_build_kernel_config(),
|
|
||||||
_build_device_config(),
|
|
||||||
_build_static_config(config),
|
|
||||||
_build_direct_config(config, interface_map),
|
|
||||||
# _build_ospf_config(config, interface_map),
|
|
||||||
_build_bgp_config(config, interface_map),
|
|
||||||
_build_radv_config(config, interface_map),
|
|
||||||
]
|
|
||||||
|
|
||||||
return '\n'.join(config_data)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_external_v4_ip(config):
|
|
||||||
"""
|
|
||||||
Determines the external IPv4 address.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
v4_id = config.external_v4_id
|
|
||||||
|
|
||||||
if v4_id:
|
|
||||||
return v4_id
|
|
||||||
else: # fallback to random value
|
|
||||||
return '0.0.%d.%d' % (random.randint(0, 255), random.randint(0, 255))
|
|
||||||
|
|
||||||
|
|
||||||
def _build_global_config(config):
|
|
||||||
"""
|
|
||||||
Generate the "global" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
retval = [
|
|
||||||
'log syslog {warning, error, info};',
|
|
||||||
'router id %s;' % _find_external_v4_ip(config),
|
|
||||||
]
|
|
||||||
return '\n'.join(retval)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_kernel_config():
|
|
||||||
"""
|
|
||||||
Generate the "kernel" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
config = """
|
|
||||||
protocol kernel {
|
|
||||||
learn;
|
|
||||||
scan time 20;
|
|
||||||
import all;
|
|
||||||
export all;
|
|
||||||
}"""
|
|
||||||
|
|
||||||
return textwrap.dedent(config).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _build_device_config():
|
|
||||||
"""
|
|
||||||
Generate the "device" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
return 'protocol device {\n scan time 10;\n}'
|
|
||||||
|
|
||||||
|
|
||||||
def _build_static_config(config):
|
|
||||||
"""
|
|
||||||
Generate the "static" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
retval = []
|
|
||||||
# TODO: setup static routes
|
|
||||||
return '\n'.join(retval).replace('\t', ' ')
|
|
||||||
|
|
||||||
|
|
||||||
def _build_direct_config(config, interface_map):
|
|
||||||
"""
|
|
||||||
Generate the "direct" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:type interface_map: dict
|
|
||||||
:param interface_map:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
tmpl = "protocol direct {\n interface %s;\n}"
|
|
||||||
retval = tmpl % ','.join(
|
|
||||||
'"%s"' % i for i in sorted(interface_map.values())
|
|
||||||
)
|
|
||||||
return textwrap.dedent(retval)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_ospf_config(config, interface_map):
|
|
||||||
"""
|
|
||||||
Generate the "ospf" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:type interface_map: dict
|
|
||||||
:param interface_map:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
retval = [
|
|
||||||
'protocol ospf {',
|
|
||||||
'\texport all;',
|
|
||||||
'\trfc1583compat yes;',
|
|
||||||
'\tarea %d {' % DEFAULT_AREA
|
|
||||||
]
|
|
||||||
|
|
||||||
for net in config.networks:
|
|
||||||
ifname = interface_map.get(net.interface.ifname)
|
|
||||||
if ifname and net.is_internal_network:
|
|
||||||
modifier = 'stub yes'
|
|
||||||
elif ifname and net.is_external_network:
|
|
||||||
modifier = 'type broadcast'
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
retval.extend([
|
|
||||||
'\t\tinterface "%s" {' % ifname,
|
|
||||||
'\t\t\tcost 10;',
|
|
||||||
'\t\t\t%s;' % modifier,
|
|
||||||
'\t\t};'
|
|
||||||
])
|
|
||||||
|
|
||||||
retval.extend([
|
|
||||||
'\t};',
|
|
||||||
'};'
|
|
||||||
])
|
|
||||||
return '\n'.join(retval).replace('\t', ' ')
|
|
||||||
|
|
||||||
|
|
||||||
def _build_bgp_config(config, interface_map):
|
|
||||||
"""
|
|
||||||
Generate the "BGP" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:type interface_map: dict
|
|
||||||
:param interface_map:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
|
|
||||||
# build the filter rule
|
|
||||||
retval = [
|
|
||||||
'filter bgp_out {',
|
|
||||||
'\tif ! (source = RTS_DEVICE) then reject;',
|
|
||||||
'\tif net ~ fc00::/7 then reject;', # filter out private addresses
|
|
||||||
]
|
|
||||||
|
|
||||||
for net in config.networks:
|
|
||||||
if not net.is_internal_network:
|
|
||||||
continue
|
|
||||||
retval.extend(
|
|
||||||
'\tif net = %s then accept;' % s.cidr
|
|
||||||
for s in net.subnets if s.cidr.version == 6 and s.gateway_ip
|
|
||||||
)
|
|
||||||
|
|
||||||
retval.extend(
|
|
||||||
[
|
|
||||||
'\telse reject;',
|
|
||||||
'}',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# build the bgp rule
|
|
||||||
for net in config.networks:
|
|
||||||
ifname = interface_map.get(net.interface.ifname)
|
|
||||||
|
|
||||||
if not net.is_external_network or not ifname:
|
|
||||||
continue
|
|
||||||
|
|
||||||
v6_subnets = (s for s in net.subnets
|
|
||||||
if s.cidr.version == 6 and s.gateway_ip)
|
|
||||||
|
|
||||||
for subnet in v6_subnets:
|
|
||||||
retval.extend([
|
|
||||||
'protocol bgp {',
|
|
||||||
'\tlocal as %d;' % config.asn,
|
|
||||||
'\tneighbor %s as %d;' % (subnet.gateway_ip,
|
|
||||||
config.neighbor_asn),
|
|
||||||
'\timport all;',
|
|
||||||
'\texport filter bgp_out;',
|
|
||||||
'\trr client;',
|
|
||||||
'}'
|
|
||||||
])
|
|
||||||
|
|
||||||
return '\n'.join(retval).replace('\t', ' ')
|
|
||||||
|
|
||||||
|
|
||||||
def _build_radv_config(config, interface_map):
|
|
||||||
"""
|
|
||||||
Generate the "radv" section of the BIRD daemon configuration.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:type interface_map: dict
|
|
||||||
:param interface_map:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
retval = [
|
|
||||||
'protocol radv {',
|
|
||||||
]
|
|
||||||
|
|
||||||
for net in config.networks:
|
|
||||||
if not net.is_tenant_network:
|
|
||||||
continue
|
|
||||||
|
|
||||||
v6_subnets = [s for s in net.subnets if s.cidr.version == 6]
|
|
||||||
|
|
||||||
if not v6_subnets:
|
|
||||||
continue
|
|
||||||
|
|
||||||
real_ifname = interface_map.get(net.interface.ifname)
|
|
||||||
|
|
||||||
if not real_ifname:
|
|
||||||
continue
|
|
||||||
|
|
||||||
retval.extend([
|
|
||||||
'\tinterface "%s" {' % real_ifname,
|
|
||||||
'\t\tmax ra interval 600;',
|
|
||||||
'\t\trdnss local yes;'
|
|
||||||
])
|
|
||||||
for subnet in v6_subnets:
|
|
||||||
retval.append('\t\tprefix %s {' % subnet.cidr)
|
|
||||||
if subnet.dhcp_enabled:
|
|
||||||
retval.append('\t\t\tautonomous off;')
|
|
||||||
retval.append('\t\t};')
|
|
||||||
|
|
||||||
if subnet.dns_nameservers:
|
|
||||||
retval.append('\t\trdnss {')
|
|
||||||
retval.append('\t\t\tlifetime mult 10;')
|
|
||||||
|
|
||||||
for ns in subnet.dns_nameservers:
|
|
||||||
retval.append('\t\t\tns %s;' % ns)
|
|
||||||
|
|
||||||
retval.append('\t\t};')
|
|
||||||
retval.append('\t};')
|
|
||||||
|
|
||||||
retval.append('}')
|
|
||||||
return '\n'.join(retval).replace('\t', ' ')
|
|
@ -1,38 +0,0 @@
|
|||||||
General {
|
|
||||||
HashSize 8192
|
|
||||||
HashLimit 65535
|
|
||||||
Syslog on
|
|
||||||
LockFile /var/lock/conntrackd.lock
|
|
||||||
UNIX {
|
|
||||||
Path /var/run/conntrackd.sock
|
|
||||||
Backlog 20
|
|
||||||
}
|
|
||||||
SocketBufferSize 262142
|
|
||||||
SocketBufferSizeMaxGrown 655355
|
|
||||||
Filter {
|
|
||||||
Protocol Accept {
|
|
||||||
TCP
|
|
||||||
}
|
|
||||||
Address Ignore {
|
|
||||||
IPv4_address 127.0.0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sync {
|
|
||||||
Mode FTFW {
|
|
||||||
}
|
|
||||||
UDP Default {
|
|
||||||
{%- if management_ip_version == 4 %}
|
|
||||||
IPv4_address {{ source_address }}
|
|
||||||
IPv4_Destination_Address {{ destination_address }}
|
|
||||||
{%- else %}
|
|
||||||
IPv6_address {{ source_address }}
|
|
||||||
IPv6_Destination_Address {{ destination_address }}
|
|
||||||
{%- endif %}
|
|
||||||
Port 3780
|
|
||||||
Interface {{ interface }}
|
|
||||||
SndSocketBuffer 24985600
|
|
||||||
RcvSocketBuffer 24985600
|
|
||||||
Checksum on
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
# Copyright (c) 2016 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
class ConntrackdManager(base.Manager):
|
|
||||||
"""
|
|
||||||
A class to provide facilities to interact with the conntrackd daemon.
|
|
||||||
"""
|
|
||||||
EXECUTABLE = 'service'
|
|
||||||
CONFIG_FILE_TEMPLATE = os.path.join(
|
|
||||||
os.path.dirname(__file__), 'conntrackd.conf.template')
|
|
||||||
|
|
||||||
# Debian defaults
|
|
||||||
CONFIG_FILE = '/etc/conntrackd/conntrackd.conf'
|
|
||||||
|
|
||||||
# Debian installs this to /usr/share/doc/examples/sync but our
|
|
||||||
# DIB recipe will install it here.
|
|
||||||
NOTIFY_SCRIPT = '/etc/conntrackd/primary-backup.sh'
|
|
||||||
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes ConntrackdManager class.
|
|
||||||
|
|
||||||
:type root_helper: str
|
|
||||||
:param root_helper: System utility used to gain escalate privileges.
|
|
||||||
"""
|
|
||||||
super(ConntrackdManager, self).__init__(root_helper)
|
|
||||||
self._config_templ = utils.load_template(self.CONFIG_FILE_TEMPLATE)
|
|
||||||
self._should_restart = False
|
|
||||||
|
|
||||||
def save_config(self, config, generic_to_host):
|
|
||||||
"""
|
|
||||||
Renders template and writes to the conntrackd file
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config: An astara_router.models.Configuration object containing
|
|
||||||
the ha_config configuration.
|
|
||||||
:param generic_to_host: A callable used to resolve generic interface
|
|
||||||
name to system interface name.
|
|
||||||
"""
|
|
||||||
|
|
||||||
mgt_interface = None
|
|
||||||
for interface in config.interfaces:
|
|
||||||
if interface.management:
|
|
||||||
mgt_interface = interface
|
|
||||||
break
|
|
||||||
mgt_addr = mgt_interface.first_v6 or mgt_interface.first_v4
|
|
||||||
ctxt = {
|
|
||||||
'source_address': str(mgt_addr),
|
|
||||||
'management_ip_version': mgt_addr.version,
|
|
||||||
'destination_address': config.ha_config['peers'][0],
|
|
||||||
'interface': generic_to_host(interface.ifname),
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
old_config_hash = utils.hash_file(self.CONFIG_FILE)
|
|
||||||
except IOError:
|
|
||||||
old_config_hash = None
|
|
||||||
|
|
||||||
utils.replace_file(
|
|
||||||
'/tmp/conntrackd.conf',
|
|
||||||
self._config_templ.render(ctxt))
|
|
||||||
utils.execute(
|
|
||||||
['mv', '/tmp/conntrackd.conf', self.CONFIG_FILE],
|
|
||||||
self.root_helper)
|
|
||||||
|
|
||||||
if old_config_hash != utils.hash_file(self.CONFIG_FILE):
|
|
||||||
self._should_restart = True
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""
|
|
||||||
Restarts the conntrackd daemon if config has been changed
|
|
||||||
"""
|
|
||||||
if not self._should_restart:
|
|
||||||
return
|
|
||||||
self.sudo('conntrackd', 'restart')
|
|
@ -1,170 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import operator
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
import netaddr
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
CONF_DIR = '/etc/dnsmasq.d'
|
|
||||||
RC_PATH = '/etc/init.d/dnsmasq'
|
|
||||||
DEFAULT_LEASE = 86400
|
|
||||||
|
|
||||||
|
|
||||||
class DHCPManager(base.Manager):
|
|
||||||
"""A class to manage dnsmasq."""
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes DHCPManager class.
|
|
||||||
|
|
||||||
:type root_helper: str
|
|
||||||
:param root_helper: System utility used to gain escalate privileges.
|
|
||||||
"""
|
|
||||||
super(DHCPManager, self).__init__(root_helper)
|
|
||||||
|
|
||||||
def delete_all_config(self):
|
|
||||||
"""
|
|
||||||
Deletes all the dnsmasq configuration files (in <CONF_DIR>) that end in
|
|
||||||
.conf.
|
|
||||||
"""
|
|
||||||
for f in os.listdir(CONF_DIR):
|
|
||||||
if f.endswith('.conf'):
|
|
||||||
utils.execute(['rm', '-f', os.path.join(CONF_DIR, f)],
|
|
||||||
self.root_helper)
|
|
||||||
|
|
||||||
def update_network_dhcp_config(self, ifname, network):
|
|
||||||
"""
|
|
||||||
Updates the dnsmasq.conf config, enabling dhcp configuration for nova
|
|
||||||
networks that are mapped to tenants and disabling networks that do not
|
|
||||||
map to tenants.
|
|
||||||
|
|
||||||
:type ifname: str
|
|
||||||
:param ifname:
|
|
||||||
:type network:
|
|
||||||
:param network:
|
|
||||||
|
|
||||||
"""
|
|
||||||
if network.is_tenant_network:
|
|
||||||
config_data = self._build_dhcp_config(ifname, network)
|
|
||||||
else:
|
|
||||||
config_data = self._build_disabled_config(ifname)
|
|
||||||
|
|
||||||
file_path = os.path.join(CONF_DIR, '%s.conf' % ifname)
|
|
||||||
utils.replace_file('/tmp/dnsmasq.conf', config_data)
|
|
||||||
utils.execute(['mv', '/tmp/dnsmasq.conf', file_path], self.root_helper)
|
|
||||||
|
|
||||||
def _build_disabled_config(self, ifname):
|
|
||||||
"""
|
|
||||||
Appends "except-interface" for <ifname>. This is used to disable an
|
|
||||||
interface in the dnsmasq file and should be called from the wrapper
|
|
||||||
update_network_dhcp_config.
|
|
||||||
|
|
||||||
:type ifname: str
|
|
||||||
:param ifname: Name of the interface to add an exception to in dnsmasq
|
|
||||||
configuration.
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
return 'except-interface=%s\n' % ifname
|
|
||||||
|
|
||||||
def _build_dhcp_config(self, ifname, network):
|
|
||||||
"""
|
|
||||||
Creates <config> containing dnsmasq configuration information for
|
|
||||||
<ifname>/<network>. Should be called from wrapper
|
|
||||||
update_network_dhcp_config.
|
|
||||||
|
|
||||||
:type ifname: str
|
|
||||||
:param ifname:
|
|
||||||
:type network:
|
|
||||||
:param network:
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
config = ['interface=%s' % ifname]
|
|
||||||
|
|
||||||
for index, subnet in enumerate(network.subnets):
|
|
||||||
if not subnet.dhcp_enabled:
|
|
||||||
continue
|
|
||||||
|
|
||||||
tag = '%s_%s' % (ifname, index)
|
|
||||||
|
|
||||||
config.append('dhcp-range=set:%s,%s,%s,%ss' %
|
|
||||||
(tag,
|
|
||||||
subnet.cidr.network,
|
|
||||||
'static',
|
|
||||||
DEFAULT_LEASE))
|
|
||||||
|
|
||||||
if subnet.cidr.version == 6:
|
|
||||||
option_label = 'option6'
|
|
||||||
else:
|
|
||||||
option_label = 'option'
|
|
||||||
|
|
||||||
config.extend(
|
|
||||||
'dhcp-option=tag:%s,%s:dns-server,%s' % (tag, option_label, s)
|
|
||||||
for s in subnet.dns_nameservers
|
|
||||||
)
|
|
||||||
|
|
||||||
config.extend(
|
|
||||||
'dhcp-option=tag:%s,%s:classless-static-route,%s,%s' %
|
|
||||||
(tag, option_label, r.destination, r.next_hop)
|
|
||||||
for r in subnet.host_routes
|
|
||||||
)
|
|
||||||
|
|
||||||
for a in network.address_allocations:
|
|
||||||
dhcp_addresses = sorted(map(netaddr.IPAddress, a.dhcp_addresses))
|
|
||||||
groups = itertools.groupby(dhcp_addresses,
|
|
||||||
key=operator.attrgetter('version'))
|
|
||||||
dhcp_addresses = [str(next(members)) for k, members in groups]
|
|
||||||
config.extend([
|
|
||||||
'dhcp-host=%s,%s,%s' % (
|
|
||||||
a.mac_address,
|
|
||||||
','.join(
|
|
||||||
'[%s]' % ip if ':' in ip else ip
|
|
||||||
for ip in dhcp_addresses
|
|
||||||
),
|
|
||||||
a.hostname
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
return '\n'.join(config)
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""
|
|
||||||
Restarts dnsmasq service using the system provided init script.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
utils.execute(['service', 'dnsmasq', 'stop'], self.root_helper)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# dnsmasq can get confused on startup
|
|
||||||
remaining = 5
|
|
||||||
while remaining:
|
|
||||||
remaining -= 1
|
|
||||||
try:
|
|
||||||
utils.execute(
|
|
||||||
['service', 'dnsmasq', 'start'], self.root_helper
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
if remaining <= 0:
|
|
||||||
raise
|
|
||||||
time.sleep(1)
|
|
@ -1,48 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
class HostnameManager(base.Manager):
|
|
||||||
EXECUTABLE = 'hostname'
|
|
||||||
|
|
||||||
def update(self, config):
|
|
||||||
self.update_hostname(config)
|
|
||||||
self.update_hosts(config)
|
|
||||||
|
|
||||||
def update_hostname(self, config):
|
|
||||||
self.sudo(config.hostname)
|
|
||||||
utils.replace_file('/tmp/hostname', config.hostname)
|
|
||||||
utils.execute(
|
|
||||||
['mv', '/tmp/hostname', '/etc/hostname'], self.root_helper
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_hosts(self, config):
|
|
||||||
mgt_addr = config.management_address
|
|
||||||
|
|
||||||
if not mgt_addr:
|
|
||||||
return
|
|
||||||
|
|
||||||
config_data = [
|
|
||||||
'127.0.0.1 localhost',
|
|
||||||
'::1 localhost ip6-localhost ip6-loopback',
|
|
||||||
'%s %s' % (mgt_addr, config.hostname)
|
|
||||||
]
|
|
||||||
utils.replace_file('/tmp/hosts', '\n'.join(config_data))
|
|
||||||
utils.execute(['mv', '/tmp/hosts', '/etc/hosts'], self.root_helper)
|
|
@ -1,611 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
import netaddr
|
|
||||||
|
|
||||||
from astara_router import models
|
|
||||||
from astara_router.drivers import base, keepalived
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
GENERIC_IFNAME = 'ge'
|
|
||||||
PHYSICAL_INTERFACES = ['lo', 'eth', 'em', 're', 'en', 'vio', 'vtnet']
|
|
||||||
ULA_PREFIX = 'fdca:3ba5:a17a:acda::/64'
|
|
||||||
|
|
||||||
|
|
||||||
class IPManager(base.Manager):
|
|
||||||
"""
|
|
||||||
A class that provides a pythonic interface to unix system network
|
|
||||||
configuration information.
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXECUTABLE = 'ip'
|
|
||||||
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""Initializes resources for the IPManager class"""
|
|
||||||
super(IPManager, self).__init__(root_helper)
|
|
||||||
self.next_generic_index = 0
|
|
||||||
self.host_mapping = {}
|
|
||||||
self.generic_mapping = {}
|
|
||||||
|
|
||||||
def ensure_mapping(self):
|
|
||||||
"""
|
|
||||||
Creates a mapping of generic interface names (e.g., ge0, ge1) to
|
|
||||||
physical interface names (eth1, eth2).
|
|
||||||
"""
|
|
||||||
self.get_interfaces()
|
|
||||||
|
|
||||||
def get_interfaces(self):
|
|
||||||
"""
|
|
||||||
Returns a list of the available network interfaces. This information
|
|
||||||
is obtained through the `ip addr show` system command.
|
|
||||||
"""
|
|
||||||
interfaces = _parse_interfaces(self.do('addr', 'show'),
|
|
||||||
filters=PHYSICAL_INTERFACES)
|
|
||||||
|
|
||||||
interfaces.sort(key=lambda x: x.ifname)
|
|
||||||
for i in interfaces:
|
|
||||||
if i.ifname not in self.host_mapping:
|
|
||||||
generic_name = 'ge%d' % self.next_generic_index
|
|
||||||
self.host_mapping[i.ifname] = generic_name
|
|
||||||
self.next_generic_index += 1
|
|
||||||
|
|
||||||
# change ifname to generic version
|
|
||||||
i.ifname = self.host_mapping[i.ifname]
|
|
||||||
self.generic_mapping = dict((v, k) for k, v in
|
|
||||||
self.host_mapping.iteritems())
|
|
||||||
|
|
||||||
return interfaces
|
|
||||||
|
|
||||||
def get_interface(self, ifname):
|
|
||||||
"""
|
|
||||||
Returns network configuration information for the requested network
|
|
||||||
interface. This information is obtained through the system command `ip
|
|
||||||
addr show <ifname>`.
|
|
||||||
|
|
||||||
:param ifname: the name of the interface to retrieve, e.g., `eth1`
|
|
||||||
:type ifname: str
|
|
||||||
:rtype: astara_router.model.Interface
|
|
||||||
"""
|
|
||||||
real_ifname = self.generic_to_host(ifname)
|
|
||||||
retval = _parse_interface(self.do('addr', 'show', real_ifname))
|
|
||||||
retval.ifname = ifname
|
|
||||||
return retval
|
|
||||||
|
|
||||||
def is_valid(self, ifname):
|
|
||||||
"""
|
|
||||||
Validates if the supplied interface is a valid system network
|
|
||||||
interface. Returns `True` if <ifname> is a valid interface. Returns
|
|
||||||
`False` if <ifname> is not a valid interface.
|
|
||||||
|
|
||||||
:param ifname: the name of the interface to retrieve, e.g., `eth1`
|
|
||||||
:type ifname: str
|
|
||||||
"""
|
|
||||||
self.ensure_mapping()
|
|
||||||
return ifname in self.generic_mapping
|
|
||||||
|
|
||||||
def generic_to_host(self, generic_name):
|
|
||||||
"""
|
|
||||||
Translates a generic interface name into the physical network interface
|
|
||||||
name.
|
|
||||||
:param ifname: the generic name to translate, e.g., `ge0`
|
|
||||||
:type ifname: str
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
self.ensure_mapping()
|
|
||||||
return self.generic_mapping.get(generic_name)
|
|
||||||
|
|
||||||
def host_to_generic(self, real_name):
|
|
||||||
"""
|
|
||||||
Translates a physical interface name into the generic network interface
|
|
||||||
name.
|
|
||||||
:param ifname: the physical name to translate, e.g., `eth0`
|
|
||||||
:type ifname: str
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
self.ensure_mapping()
|
|
||||||
return self.host_mapping.get(real_name)
|
|
||||||
|
|
||||||
def update_interfaces(self, interfaces):
|
|
||||||
"""
|
|
||||||
Wrapper function that accepts a list of interfaces and iterates over
|
|
||||||
them, calling update_interface(<interface>) in order to update
|
|
||||||
their configuration.
|
|
||||||
"""
|
|
||||||
for i in interfaces:
|
|
||||||
self.update_interface(i)
|
|
||||||
|
|
||||||
def up(self, interface):
|
|
||||||
"""
|
|
||||||
Sets the administrative mode for the network link on interface
|
|
||||||
<interface> to "up".
|
|
||||||
:param interface: the interface to mark up
|
|
||||||
:type interface: astara_router.models.Interface
|
|
||||||
"""
|
|
||||||
real_ifname = self.generic_to_host(interface.ifname)
|
|
||||||
self.sudo('link', 'set', real_ifname, 'up')
|
|
||||||
return self.get_interface(interface.ifname)
|
|
||||||
|
|
||||||
def down(self, interface):
|
|
||||||
"""
|
|
||||||
Sets the administrative mode for the network link on interface
|
|
||||||
<interface> to "down".
|
|
||||||
:param interface: the interface to mark down
|
|
||||||
:type interface: astara_router.models.Interface
|
|
||||||
"""
|
|
||||||
real_ifname = self.generic_to_host(interface.ifname)
|
|
||||||
self.sudo('link', 'set', real_ifname, 'down')
|
|
||||||
|
|
||||||
def set_mtu(self, interface):
|
|
||||||
"""
|
|
||||||
Sets the mtu on interface <interface> to mtu.
|
|
||||||
:param interface: the interface to set mtu
|
|
||||||
:type interface: astara_router.models.Interface
|
|
||||||
"""
|
|
||||||
real_ifname = self.generic_to_host(interface.ifname)
|
|
||||||
self.sudo('link', 'set', real_ifname, 'mtu', str(interface.mtu))
|
|
||||||
|
|
||||||
def update_interface(self, interface, ignore_link_local=True):
|
|
||||||
"""
|
|
||||||
Updates a network interface, particularly its addresses
|
|
||||||
:param interface: the interface to update
|
|
||||||
:type interface: astara_router.models.Interface
|
|
||||||
:param ignore_link_local: When True, link local addresses will not be
|
|
||||||
added/removed
|
|
||||||
:type ignore_link_local: bool
|
|
||||||
"""
|
|
||||||
real_ifname = self.generic_to_host(interface.ifname)
|
|
||||||
old_interface = self.get_interface(interface.ifname)
|
|
||||||
|
|
||||||
if ignore_link_local:
|
|
||||||
interface.addresses = [a for a in interface.addresses
|
|
||||||
if not a.is_link_local()]
|
|
||||||
old_interface.addresses = [a for a in old_interface.addresses
|
|
||||||
if not a.is_link_local()]
|
|
||||||
# Must update primary before aliases otherwise will lose address
|
|
||||||
# in case where primary and alias are swapped.
|
|
||||||
self._update_addresses(real_ifname, interface, old_interface)
|
|
||||||
|
|
||||||
if interface.mtu is not None and old_interface.mtu != interface.mtu:
|
|
||||||
self.set_mtu(interface)
|
|
||||||
|
|
||||||
def _update_addresses(self, real_ifname, interface, old_interface):
|
|
||||||
"""
|
|
||||||
Compare the state of an interface, and add/remove address that have
|
|
||||||
changed.
|
|
||||||
:param real_ifname: the name of the interface to modify
|
|
||||||
:param real_ifname: str
|
|
||||||
:param interface: the new interface reference
|
|
||||||
:type interface: astara_router.models.Interface
|
|
||||||
:param old_interface: the reference to the current network interface
|
|
||||||
:type old_interface: astara_router.models.Interface
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _gen_cmd(cmd, address):
|
|
||||||
"""
|
|
||||||
Generates an `ip addr (add|del) <cidr> dev <ifname>` command.
|
|
||||||
"""
|
|
||||||
family = {4: 'inet', 6: 'inet6'}[address[0].version]
|
|
||||||
args = ['addr', cmd, '%s/%s' % (address[0], address[1])]
|
|
||||||
if family == 'inet' and cmd == 'add':
|
|
||||||
args += ['brd', '+']
|
|
||||||
args += ['dev', real_ifname]
|
|
||||||
if family == 'inet6':
|
|
||||||
args = ['-6'] + args
|
|
||||||
return args
|
|
||||||
|
|
||||||
add = functools.partial(_gen_cmd, 'add')
|
|
||||||
delete = functools.partial(_gen_cmd, 'del')
|
|
||||||
|
|
||||||
self._update_set(real_ifname, interface, old_interface,
|
|
||||||
'all_addresses', add, delete)
|
|
||||||
|
|
||||||
def _update_set(self, real_ifname, interface, old_interface, attribute,
|
|
||||||
fmt_args_add, fmt_args_delete):
|
|
||||||
"""
|
|
||||||
Compare the set of addresses (the current set and the desired set)
|
|
||||||
for an interface and generate a series of `ip addr add` and `ip addr
|
|
||||||
del` commands.
|
|
||||||
"""
|
|
||||||
next_set = set((i.ip, i.prefixlen)
|
|
||||||
for i in getattr(interface, attribute))
|
|
||||||
prev_set = set((i.ip, i.prefixlen)
|
|
||||||
for i in getattr(old_interface, attribute))
|
|
||||||
|
|
||||||
if next_set == prev_set:
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in (next_set - prev_set):
|
|
||||||
self.sudo(*fmt_args_add(item))
|
|
||||||
self.up(interface)
|
|
||||||
|
|
||||||
for item in (prev_set - next_set):
|
|
||||||
try:
|
|
||||||
self.sudo(*fmt_args_delete(item))
|
|
||||||
except RuntimeError:
|
|
||||||
LOG.warning('IP could not be deleted: %s' % item)
|
|
||||||
ip, prefix = item
|
|
||||||
if ip.version == 4:
|
|
||||||
self._delete_conntrack_state(ip)
|
|
||||||
|
|
||||||
def update_default_gateway(self, config):
|
|
||||||
"""
|
|
||||||
Sets the default gateway for v4 and v6 via the use of `ip route add`.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
"""
|
|
||||||
# Track whether we have set the default gateways, by IP
|
|
||||||
# version.
|
|
||||||
gw_set = {
|
|
||||||
4: False,
|
|
||||||
6: False,
|
|
||||||
}
|
|
||||||
|
|
||||||
ifname = None
|
|
||||||
for net in config.networks:
|
|
||||||
if not net.is_external_network:
|
|
||||||
continue
|
|
||||||
ifname = net.interface.ifname
|
|
||||||
|
|
||||||
# The default v4 gateway is pulled out as a special case
|
|
||||||
# because we only want one but we might have multiple v4
|
|
||||||
# subnets on the external network. However, sometimes the RUG
|
|
||||||
# can't figure out what that value is, because it thinks we
|
|
||||||
# don't have any external IP addresses, yet. In that case, it
|
|
||||||
# doesn't give us a default.
|
|
||||||
if config.default_v4_gateway:
|
|
||||||
self._set_default_gateway(config.default_v4_gateway, ifname)
|
|
||||||
gw_set[4] = True
|
|
||||||
|
|
||||||
# Look through our networks and make sure we have a default
|
|
||||||
# gateway set for each IP version, if we have an IP for that
|
|
||||||
# version on the external net. If we haven't already set the
|
|
||||||
# v4 gateway, this picks the gateway for the first subnet we
|
|
||||||
# find, which might be wrong.
|
|
||||||
for net in config.networks:
|
|
||||||
if not net.is_external_network:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for subnet in net.subnets:
|
|
||||||
if subnet.gateway_ip and not gw_set[subnet.gateway_ip.version]:
|
|
||||||
self._set_default_gateway(
|
|
||||||
subnet.gateway_ip,
|
|
||||||
net.interface.ifname
|
|
||||||
)
|
|
||||||
gw_set[subnet.gateway_ip.version] = True
|
|
||||||
|
|
||||||
def update_host_routes(self, config, cache):
|
|
||||||
"""
|
|
||||||
Update the network routes. This is primarily used to support static
|
|
||||||
routes that users provide to neutron.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param cache: a dbm cache for storing the "last applied routes".
|
|
||||||
Because Linux does not differentiate user-provided routes
|
|
||||||
from, for example, the default gateway, this is necessary
|
|
||||||
so that subsequent calls to this method can determine
|
|
||||||
"what changed" for the user-provided routes.
|
|
||||||
:type cache: dogpile.cache.region.CacheRegion
|
|
||||||
"""
|
|
||||||
db = cache.get_or_create('host_routes', lambda: {})
|
|
||||||
for net in config.networks:
|
|
||||||
|
|
||||||
# For each subnet...
|
|
||||||
for subnet in net.subnets:
|
|
||||||
cidr = str(subnet.cidr)
|
|
||||||
|
|
||||||
# determine the set of previously written routes for this cidr
|
|
||||||
if cidr not in db:
|
|
||||||
db[cidr] = set()
|
|
||||||
|
|
||||||
current = db[cidr]
|
|
||||||
|
|
||||||
# build a set of new routes for this cidr
|
|
||||||
latest = set()
|
|
||||||
for r in subnet.host_routes:
|
|
||||||
latest.add((r.destination, r.next_hop))
|
|
||||||
|
|
||||||
# If the set of previously written routes contains routes that
|
|
||||||
# aren't defined in the new config, run commands to delete them
|
|
||||||
for x in current - latest:
|
|
||||||
if self._alter_route(net.interface.ifname, 'del', *x):
|
|
||||||
current.remove(x)
|
|
||||||
|
|
||||||
# If the new config contains routes that aren't defined in the
|
|
||||||
# set of previously written routes, run commands to add them
|
|
||||||
for x in latest - current:
|
|
||||||
if self._alter_route(net.interface.ifname, 'add', *x):
|
|
||||||
current.add(x)
|
|
||||||
|
|
||||||
if not current:
|
|
||||||
del db[cidr]
|
|
||||||
|
|
||||||
cache.set('host_routes', db)
|
|
||||||
|
|
||||||
def _get_default_gateway(self, version):
|
|
||||||
"""
|
|
||||||
Gets the default gateway.
|
|
||||||
|
|
||||||
:param version: the IP version, 4 or 6
|
|
||||||
:type version: int
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
cmd_out = self.sudo('-%s' % version, 'route', 'show')
|
|
||||||
except:
|
|
||||||
# assume the route is missing and use defaults
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for l in cmd_out.splitlines():
|
|
||||||
l = l.strip()
|
|
||||||
if l.startswith('default'):
|
|
||||||
match = re.search('via (?P<gateway>[^ ]+)', l)
|
|
||||||
if match:
|
|
||||||
return match.group('gateway')
|
|
||||||
|
|
||||||
def _set_default_gateway(self, gateway_ip, ifname):
|
|
||||||
"""
|
|
||||||
Sets the default gateway.
|
|
||||||
|
|
||||||
:param gateway_ip: the IP address to set as the default gateway_ip
|
|
||||||
:type gateway_ip: netaddr.IPAddress
|
|
||||||
:param ifname: the interface name (in our case, of the external
|
|
||||||
network)
|
|
||||||
:type ifname: str
|
|
||||||
"""
|
|
||||||
version = 4
|
|
||||||
if gateway_ip.version == 6:
|
|
||||||
version = 6
|
|
||||||
current = self._get_default_gateway(version)
|
|
||||||
desired = str(gateway_ip)
|
|
||||||
ifname = self.generic_to_host(ifname)
|
|
||||||
|
|
||||||
if current and current != desired:
|
|
||||||
# Remove the current gateway and add the desired one
|
|
||||||
self.sudo(
|
|
||||||
'-%s' % version, 'route', 'del', 'default', 'via', current,
|
|
||||||
'dev', ifname
|
|
||||||
)
|
|
||||||
return self.sudo(
|
|
||||||
'-%s' % version, 'route', 'add', 'default', 'via', desired,
|
|
||||||
'dev', ifname
|
|
||||||
)
|
|
||||||
if not current:
|
|
||||||
# Add the desired gateway
|
|
||||||
return self.sudo(
|
|
||||||
'-%s' % version, 'route', 'add', 'default', 'via', desired,
|
|
||||||
'dev', ifname
|
|
||||||
)
|
|
||||||
|
|
||||||
def _alter_route(self, ifname, action, destination, next_hop):
|
|
||||||
"""
|
|
||||||
Apply/remove a custom (generally, user-supplied) route using the `ip
|
|
||||||
route add/delete` command.
|
|
||||||
|
|
||||||
:param ifname: The name of the interface on which to alter the route
|
|
||||||
:type ifname: str
|
|
||||||
:param action: The action, 'add' or 'del'
|
|
||||||
:type action: str
|
|
||||||
:param destination: The destination CIDR
|
|
||||||
:type destination: netaddr.IPNetwork
|
|
||||||
:param next_hop: The next hop IP addressj
|
|
||||||
:type next_hop: netaddr.IPAddress
|
|
||||||
"""
|
|
||||||
version = destination.version
|
|
||||||
ifname = self.generic_to_host(ifname)
|
|
||||||
try:
|
|
||||||
LOG.debug(self.sudo(
|
|
||||||
'-%s' % version, 'route', action, str(destination), 'via',
|
|
||||||
str(next_hop), 'dev', ifname
|
|
||||||
))
|
|
||||||
return True
|
|
||||||
except RuntimeError as e:
|
|
||||||
# Since these are user-supplied custom routes, it's very possible
|
|
||||||
# that adding/removing them will fail. A failure to apply one of
|
|
||||||
# these custom rules, however, should *not* cause an overall router
|
|
||||||
# failure.
|
|
||||||
LOG.warning('Route could not be %sed: %s' % (action, unicode(e)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def disable_duplicate_address_detection(self, network):
|
|
||||||
"""
|
|
||||||
Disabled duplicate address detection for a specific interface.
|
|
||||||
|
|
||||||
:type network: astara.models.Network
|
|
||||||
"""
|
|
||||||
# For non-external networks, duplicate address detection isn't
|
|
||||||
# necessary (and it sometimes results in race conditions for services
|
|
||||||
# that attempt to bind to addresses before they're ready).
|
|
||||||
|
|
||||||
if network.network_type != network.TYPE_EXTERNAL:
|
|
||||||
real_ifname = self.generic_to_host(network.interface.ifname)
|
|
||||||
try:
|
|
||||||
utils.execute([
|
|
||||||
'sysctl', '-w', 'net.ipv6.conf.%s.accept_dad=0'
|
|
||||||
% real_ifname
|
|
||||||
], self.root_helper)
|
|
||||||
except RuntimeError:
|
|
||||||
LOG.debug(
|
|
||||||
'Failed to disable v6 dad on %s' % real_ifname
|
|
||||||
)
|
|
||||||
|
|
||||||
def _delete_conntrack_state(self, ip):
|
|
||||||
"""
|
|
||||||
Explicitly remove an IP from in-kernel connection tracking.
|
|
||||||
|
|
||||||
:param ip: The IP address to remove
|
|
||||||
:type ip: netaddr.IPAddress
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If no flow entries are deleted, `conntrack -D` will return 1
|
|
||||||
try:
|
|
||||||
utils.execute(['conntrack', '-D', '-d', str(ip)], self.root_helper)
|
|
||||||
except RuntimeError:
|
|
||||||
LOG.debug(
|
|
||||||
'Failed deleting ingress connection state of %s' % ip
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
utils.execute(['conntrack', '-D', '-q', str(ip)], self.root_helper)
|
|
||||||
except RuntimeError:
|
|
||||||
LOG.debug(
|
|
||||||
'Failed deleting egress connection state of %s' % ip
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_rug_address():
|
|
||||||
""" Return the RUG address """
|
|
||||||
net = netaddr.IPNetwork(ULA_PREFIX)
|
|
||||||
return str(netaddr.IPAddress(net.first + 1))
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_interfaces(data, filters=None):
|
|
||||||
"""
|
|
||||||
Parse the output of `ip addr show`.
|
|
||||||
|
|
||||||
:param data: the output of `ip addr show`
|
|
||||||
:type data: str
|
|
||||||
:param filter: a list of valid interface names to match on
|
|
||||||
:type data: list of str
|
|
||||||
:rtype: list of astara_router.models.Interface
|
|
||||||
"""
|
|
||||||
retval = []
|
|
||||||
for iface_data in re.split('(^|\n)(?=[0-9]+: \w+\d{0,3}:)', data):
|
|
||||||
if not iface_data.strip():
|
|
||||||
continue
|
|
||||||
number, interface = iface_data.split(': ', 1)
|
|
||||||
|
|
||||||
# FIXME (mark): the logic works, but should be more readable
|
|
||||||
for f in filters or ['']:
|
|
||||||
if f == '':
|
|
||||||
break
|
|
||||||
elif interface.startswith(f) and interface[len(f)].isdigit():
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
retval.append(_parse_interface(iface_data))
|
|
||||||
return retval
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_interface(data):
|
|
||||||
"""
|
|
||||||
Parse details for an interface, given its data from `ip addr show <ifname>`
|
|
||||||
|
|
||||||
:rtype: astara_router.models.Interface
|
|
||||||
"""
|
|
||||||
retval = dict(addresses=[])
|
|
||||||
for line in data.split('\n'):
|
|
||||||
if line.startswith(' '):
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith('inet'):
|
|
||||||
retval['addresses'].append(_parse_inet(line))
|
|
||||||
elif 'link/ether' in line:
|
|
||||||
retval['lladdr'] = _parse_lladdr(line)
|
|
||||||
else:
|
|
||||||
retval.update(_parse_head(line))
|
|
||||||
|
|
||||||
return models.Interface.from_dict(retval)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_head(line):
|
|
||||||
"""
|
|
||||||
Parse the line of `ip addr show` that contains the interface name, MTU, and
|
|
||||||
flags.
|
|
||||||
"""
|
|
||||||
retval = {}
|
|
||||||
m = re.match(
|
|
||||||
'[0-9]+: (?P<if>\w+\d{1,3}): <(?P<flags>[^>]+)> mtu (?P<mtu>[0-9]+)',
|
|
||||||
line
|
|
||||||
)
|
|
||||||
if m:
|
|
||||||
retval['ifname'] = m.group('if')
|
|
||||||
retval['mtu'] = int(m.group('mtu'))
|
|
||||||
retval['flags'] = m.group('flags').split(',')
|
|
||||||
return retval
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_inet(line):
|
|
||||||
"""
|
|
||||||
Parse a line of `ip addr show` that contains an address.
|
|
||||||
"""
|
|
||||||
tokens = line.split()
|
|
||||||
return netaddr.IPNetwork(tokens[1])
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_lladdr(line):
|
|
||||||
"""
|
|
||||||
Parse the line of `ip addr show` that contains the hardware address.
|
|
||||||
"""
|
|
||||||
tokens = line.split()
|
|
||||||
return tokens[1]
|
|
||||||
|
|
||||||
|
|
||||||
class VRRPIPManager(IPManager):
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
super(VRRPIPManager, self).__init__(root_helper)
|
|
||||||
self.keepalived = keepalived.KeepalivedManager(root_helper)
|
|
||||||
self.ensure_mapping()
|
|
||||||
|
|
||||||
def set_peers(self, peers):
|
|
||||||
self.keepalived.peers = peers
|
|
||||||
|
|
||||||
def set_priority(self, priority):
|
|
||||||
self.keepalived.set_priority(priority)
|
|
||||||
|
|
||||||
def update_interfaces(self, interfaces):
|
|
||||||
for interface in interfaces:
|
|
||||||
if interface.management:
|
|
||||||
# the mgt interface is not managed as a vip, but
|
|
||||||
# it used for keepalived mcast cluster comms
|
|
||||||
self.update_interface(interface)
|
|
||||||
self.keepalived.set_management_address(
|
|
||||||
address=interface.first_v4 or interface.first_v6)
|
|
||||||
else:
|
|
||||||
self.up(interface)
|
|
||||||
self.keepalived.add_vrrp_instance(
|
|
||||||
interface=self.generic_to_host(interface.ifname),
|
|
||||||
addresses=interface.all_addresses)
|
|
||||||
|
|
||||||
def _set_default_gateway(self, gateway_ip, ifname):
|
|
||||||
"""
|
|
||||||
Sets the default gateway.
|
|
||||||
|
|
||||||
:param gateway_ip: the IP address to set as the default gateway_ip
|
|
||||||
:type gateway_ip: netaddr.IPAddress
|
|
||||||
:param ifname: the interface name (in our case, of the external
|
|
||||||
network)
|
|
||||||
:type ifname: str
|
|
||||||
"""
|
|
||||||
version = 4
|
|
||||||
if gateway_ip.version == 6:
|
|
||||||
version = 6
|
|
||||||
self.keepalived.set_default_gateway(
|
|
||||||
ip_version=version, gateway_ip=gateway_ip,
|
|
||||||
interface=self.generic_to_host(ifname))
|
|
||||||
|
|
||||||
def update_host_routes(self, config, cache):
|
|
||||||
# XXX TODO
|
|
||||||
return
|
|
||||||
|
|
||||||
def reload(self):
|
|
||||||
self.keepalived.reload()
|
|
@ -1,490 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import re
|
|
||||||
import itertools
|
|
||||||
import os
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router.models import Network
|
|
||||||
from astara_router import settings, utils
|
|
||||||
|
|
||||||
|
|
||||||
class Rule(object):
|
|
||||||
|
|
||||||
def __init__(self, rule, ip_version=None):
|
|
||||||
self.rule = rule
|
|
||||||
self.ip_version = ip_version
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.rule
|
|
||||||
|
|
||||||
@property
|
|
||||||
def for_v4(self):
|
|
||||||
return self.ip_version in (None, 4)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def for_v6(self):
|
|
||||||
return self.ip_version in (None, 6)
|
|
||||||
|
|
||||||
|
|
||||||
class IPTablesManager(base.Manager):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
def save_config(self, config, interface_map):
|
|
||||||
'''
|
|
||||||
Save iptables-persistent firewall rules to disk.
|
|
||||||
|
|
||||||
:param config: The astara configuration to save to disk
|
|
||||||
:type config: astara.rug.models.Configuration
|
|
||||||
:param interface_map: A mapping of virtual ('ge0') to physical ('eth0')
|
|
||||||
interface names
|
|
||||||
:type interface_map: dict
|
|
||||||
'''
|
|
||||||
rules = itertools.chain(
|
|
||||||
self._build_filter_table(config),
|
|
||||||
self._build_nat_table(config),
|
|
||||||
self._build_mangle_table(config),
|
|
||||||
self._build_raw_table(config)
|
|
||||||
)
|
|
||||||
|
|
||||||
for version, rules in zip((4, 6), itertools.tee(rules)):
|
|
||||||
data = '\n'.join(map(
|
|
||||||
str,
|
|
||||||
[r for r in rules if getattr(r, 'for_v%s' % version)]
|
|
||||||
))
|
|
||||||
|
|
||||||
# Map virtual interface names
|
|
||||||
real_name = interface_map.get('ge0')[:-1]
|
|
||||||
ifname_re = '\-(?P<flag>i|o)(?P<ws>[\s!])(?P<not>!?)(?P<if>ge)(?P<no>\d+)' # noqa
|
|
||||||
ifname_sub = r'-\g<flag>\g<ws>\g<not>%s\g<no>' % real_name
|
|
||||||
data = re.sub(ifname_re, ifname_sub, data) + '\n'
|
|
||||||
|
|
||||||
utils.replace_file('/tmp/ip%stables.rules' % version, data)
|
|
||||||
|
|
||||||
utils.execute([
|
|
||||||
'mv',
|
|
||||||
'/tmp/ip%stables.rules' % version,
|
|
||||||
'/etc/iptables/rules.v%s' % version
|
|
||||||
], self.root_helper)
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
'''
|
|
||||||
Reload firewall rules via [netfilter/iptables]-persistent
|
|
||||||
Note that at some point iptables-persistent merged into
|
|
||||||
netfilter-persistent as a plugin, so use that instead if it is
|
|
||||||
available
|
|
||||||
'''
|
|
||||||
_init = '%s-persistent'
|
|
||||||
if os.path.isfile('/etc/init.d/netfilter-persistent'):
|
|
||||||
init = _init % 'netfilter'
|
|
||||||
else:
|
|
||||||
init = _init % 'iptables'
|
|
||||||
|
|
||||||
utils.execute(
|
|
||||||
['service', init, 'restart'],
|
|
||||||
self.root_helper
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_rules(self):
|
|
||||||
'''
|
|
||||||
Return the output of `iptables` and `ip6tables`.
|
|
||||||
This function is used by astara orchestrator -> HTTP as a test for
|
|
||||||
"router aliveness".
|
|
||||||
|
|
||||||
:rtype: str
|
|
||||||
'''
|
|
||||||
v4 = utils.execute(['iptables', '-L', '-n'], self.root_helper)
|
|
||||||
v6 = utils.execute(['ip6tables', '-L', '-n'], self.root_helper)
|
|
||||||
return v4 + v6
|
|
||||||
|
|
||||||
def get_external_network(self, config):
|
|
||||||
'''
|
|
||||||
Returns the external network
|
|
||||||
|
|
||||||
:rtype: astara_router.models.Network
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
return self.networks_by_type(config, Network.TYPE_EXTERNAL)[0]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_management_network(self, config):
|
|
||||||
'''
|
|
||||||
Returns the management network
|
|
||||||
|
|
||||||
:rtype: astara_router.models.Network
|
|
||||||
'''
|
|
||||||
return self.networks_by_type(config, Network.TYPE_MANAGEMENT)[0]
|
|
||||||
|
|
||||||
def get_internal_networks(self, config):
|
|
||||||
'''
|
|
||||||
Returns the internal networks
|
|
||||||
|
|
||||||
:rtype: [astara_router.models.Network]
|
|
||||||
'''
|
|
||||||
return self.networks_by_type(config, Network.TYPE_INTERNAL)
|
|
||||||
|
|
||||||
def networks_by_type(self, config, type):
|
|
||||||
'''
|
|
||||||
Returns the external network
|
|
||||||
|
|
||||||
:rtype: astara_router.models.Interface
|
|
||||||
'''
|
|
||||||
return filter(lambda n: n.network_type == type, config.networks)
|
|
||||||
|
|
||||||
def _build_filter_table(self, config):
|
|
||||||
'''
|
|
||||||
Build a list of iptables and ip6tables rules to be written to disk.
|
|
||||||
|
|
||||||
:param config: the astara configuration object:
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param rules: the list of rules to append to
|
|
||||||
:type rules: a list of astara_router.drivers.iptables.Rule objects
|
|
||||||
'''
|
|
||||||
return itertools.chain(
|
|
||||||
self._build_default_filter_rules(),
|
|
||||||
self._build_management_filter_rules(config),
|
|
||||||
self._build_internal_network_filter_rules(config),
|
|
||||||
self._build_vpn_filter_rules(config),
|
|
||||||
[Rule('COMMIT')]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _build_default_filter_rules(self):
|
|
||||||
'''
|
|
||||||
Build rules for default filter policies and ICMP handling
|
|
||||||
'''
|
|
||||||
return (
|
|
||||||
Rule('*filter'),
|
|
||||||
Rule(':INPUT DROP [0:0]'),
|
|
||||||
Rule(':FORWARD ACCEPT [0:0]'),
|
|
||||||
Rule(':OUTPUT ACCEPT [0:0]'),
|
|
||||||
Rule('-A INPUT -i lo -j ACCEPT'),
|
|
||||||
Rule(
|
|
||||||
'-A INPUT -p icmp --icmp-type echo-request -j ACCEPT',
|
|
||||||
ip_version=4
|
|
||||||
),
|
|
||||||
Rule(
|
|
||||||
'-A INPUT -p icmpv6 -j ACCEPT',
|
|
||||||
ip_version=6
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _build_management_filter_rules(self, config):
|
|
||||||
'''
|
|
||||||
Add rules specific to the management network, like allowances for SSH,
|
|
||||||
the HTTP API, and metadata proxying on the management interface.
|
|
||||||
'''
|
|
||||||
rules = []
|
|
||||||
|
|
||||||
for network in self.networks_by_type(config, Network.TYPE_MANAGEMENT):
|
|
||||||
|
|
||||||
# Allow established mgt traffic
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A INPUT -i %s -m state --state RELATED,ESTABLISHED -j ACCEPT'
|
|
||||||
% network.interface.ifname
|
|
||||||
))
|
|
||||||
|
|
||||||
# Open SSH, the HTTP API (5000) and the Nova metadata proxy (9697)
|
|
||||||
for port in (
|
|
||||||
settings.SSH, settings.API_SERVICE,
|
|
||||||
settings.ORCHESTRATOR_METADATA_PORT
|
|
||||||
):
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A INPUT -i %s -p tcp -m tcp --dport %s -j ACCEPT' % (
|
|
||||||
network.interface.ifname,
|
|
||||||
port
|
|
||||||
), ip_version=6
|
|
||||||
))
|
|
||||||
|
|
||||||
# Disallow any other management network traffic
|
|
||||||
rules.append(Rule('-A INPUT -i !%s -d %s -j DROP' % (
|
|
||||||
network.interface.ifname,
|
|
||||||
network.interface.first_v6
|
|
||||||
), ip_version=6))
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_internal_network_filter_rules(self, config):
|
|
||||||
'''
|
|
||||||
Add rules specific to private tenant networks.
|
|
||||||
'''
|
|
||||||
rules = []
|
|
||||||
ext_net = self.get_external_network(config)
|
|
||||||
if ext_net:
|
|
||||||
ext_if = ext_net.interface
|
|
||||||
else:
|
|
||||||
ext_if = None
|
|
||||||
|
|
||||||
for network in self.get_internal_networks(config):
|
|
||||||
|
|
||||||
for version, address, dhcp_port in (
|
|
||||||
(4, network.interface.first_v4, settings.DHCP),
|
|
||||||
(6, network.interface.first_v6, settings.DHCPV6)
|
|
||||||
):
|
|
||||||
if address:
|
|
||||||
# Allow DHCP
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A INPUT -i %s -p udp -m udp --dport %s -j ACCEPT' % (
|
|
||||||
network.interface.ifname,
|
|
||||||
dhcp_port
|
|
||||||
), ip_version=version
|
|
||||||
))
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A INPUT -i %s -p tcp -m tcp --dport %s -j ACCEPT' % (
|
|
||||||
network.interface.ifname,
|
|
||||||
dhcp_port
|
|
||||||
), ip_version=version
|
|
||||||
))
|
|
||||||
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A INPUT -i %s -j ACCEPT' % network.interface.ifname
|
|
||||||
))
|
|
||||||
if ext_if:
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A INPUT -i %s -m state '
|
|
||||||
'--state RELATED,ESTABLISHED -j ACCEPT' % ext_if.ifname
|
|
||||||
))
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_vpn_filter_rules(self, config):
|
|
||||||
rules = []
|
|
||||||
ext_net = self.get_external_network(config)
|
|
||||||
if ext_net:
|
|
||||||
ext_if = ext_net.interface
|
|
||||||
else:
|
|
||||||
ext_net = None
|
|
||||||
|
|
||||||
if ext_net is None or not config.vpn:
|
|
||||||
return rules
|
|
||||||
|
|
||||||
template = (
|
|
||||||
('-A INPUT -i %%s -p udp -m udp --dport %d -j ACCEPT ' %
|
|
||||||
settings.ISAKMP),
|
|
||||||
('-A INPUT -i %%s -p udp -m udp --dport %d -j ACCEPT ' %
|
|
||||||
settings.IPSEC_NAT_T),
|
|
||||||
'-A INPUT -i %s -p esp -j ACCEPT',
|
|
||||||
'-A INPUT -i %s -p ah -j ACCEPT'
|
|
||||||
)
|
|
||||||
|
|
||||||
for version in (4, 6):
|
|
||||||
rules.extend(
|
|
||||||
(Rule(t % ext_if.ifname, ip_version=version) for t in template)
|
|
||||||
)
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_nat_table(self, config):
|
|
||||||
'''
|
|
||||||
Add rules for generic v4 NAT for the internal tenant networks
|
|
||||||
'''
|
|
||||||
rules = [
|
|
||||||
Rule('*nat', ip_version=4),
|
|
||||||
]
|
|
||||||
|
|
||||||
rules.extend(self._build_public_snat_chain(config))
|
|
||||||
|
|
||||||
rules.extend([
|
|
||||||
Rule(':PREROUTING ACCEPT [0:0]', ip_version=4),
|
|
||||||
Rule(':INPUT ACCEPT [0:0]', ip_version=4),
|
|
||||||
Rule(':OUTPUT ACCEPT [0:0]', ip_version=4),
|
|
||||||
Rule(':POSTROUTING ACCEPT [0:0]', ip_version=4),
|
|
||||||
])
|
|
||||||
|
|
||||||
rules.extend(self._build_floating_ips(config))
|
|
||||||
|
|
||||||
rules.extend(self._build_v4_nat(config))
|
|
||||||
|
|
||||||
rules.append(Rule('COMMIT', ip_version=4))
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_v4_nat(self, config):
|
|
||||||
rules = []
|
|
||||||
|
|
||||||
for network in self.get_internal_networks(config):
|
|
||||||
if network.interface.first_v4:
|
|
||||||
# Forward metadata requests on the management interface
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A PREROUTING -i %s -d %s -p tcp -m tcp '
|
|
||||||
'--dport %s -j DNAT --to-destination %s:%s' % (
|
|
||||||
network.interface.ifname,
|
|
||||||
settings.METADATA_DEST_ADDRESS,
|
|
||||||
settings.HTTP,
|
|
||||||
network.interface.first_v4,
|
|
||||||
settings.internal_metadata_port(
|
|
||||||
network.interface.ifname
|
|
||||||
)
|
|
||||||
), ip_version=4
|
|
||||||
))
|
|
||||||
|
|
||||||
# Add a masquerade catch-all for VMs without floating IPs
|
|
||||||
ext_net = self.get_external_network(config)
|
|
||||||
if ext_net:
|
|
||||||
ext_if = ext_net.interface
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A POSTROUTING -o %s -j MASQUERADE' % (
|
|
||||||
ext_if.ifname
|
|
||||||
), ip_version=4
|
|
||||||
))
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_floating_ips(self, config):
|
|
||||||
'''
|
|
||||||
Add rules for neutron FloatingIPs.
|
|
||||||
'''
|
|
||||||
rules = []
|
|
||||||
ext_net = self.get_external_network(config)
|
|
||||||
if ext_net:
|
|
||||||
ext_if = ext_net.interface
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# NAT floating IP addresses
|
|
||||||
for fip in ext_net.floating_ips:
|
|
||||||
|
|
||||||
# Neutron has a bug whereby you can create a floating ip that has
|
|
||||||
# mixed IP versions between the fixed and floating address. If
|
|
||||||
# people create these accidentally, just ignore them (because
|
|
||||||
# iptables will barf if it encounters them)
|
|
||||||
if fip.fixed_ip.version == fip.floating_ip.version:
|
|
||||||
if ext_if:
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A PREROUTING -i %s -d %s -j DNAT --to-destination %s'
|
|
||||||
% (
|
|
||||||
ext_if.ifname,
|
|
||||||
fip.floating_ip,
|
|
||||||
fip.fixed_ip
|
|
||||||
), ip_version=4
|
|
||||||
))
|
|
||||||
for network in self.get_internal_networks(config):
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A PREROUTING -i %s -d %s -j DNAT '
|
|
||||||
'--to-destination %s' % (
|
|
||||||
network.interface.ifname,
|
|
||||||
fip.floating_ip,
|
|
||||||
fip.fixed_ip
|
|
||||||
), ip_version=4
|
|
||||||
))
|
|
||||||
|
|
||||||
if rules:
|
|
||||||
for network in self.get_internal_networks(config):
|
|
||||||
for subnet in network.subnets:
|
|
||||||
if subnet.cidr.version == 4:
|
|
||||||
rules.append(
|
|
||||||
Rule('-A POSTROUTING -s %s -j PUBLIC_SNAT' % (
|
|
||||||
subnet.cidr
|
|
||||||
), ip_version=4)
|
|
||||||
)
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_public_snat_chain(self, config):
|
|
||||||
'''
|
|
||||||
Build a chain for SNAT for neutron FloatingIPs. This chain ignores NAT
|
|
||||||
for traffic marked as private.
|
|
||||||
'''
|
|
||||||
external_network = self.get_external_network(config)
|
|
||||||
if not external_network:
|
|
||||||
return []
|
|
||||||
|
|
||||||
rules = [
|
|
||||||
Rule(':PUBLIC_SNAT - [0:0]', ip_version=4),
|
|
||||||
Rule(
|
|
||||||
'-A PUBLIC_SNAT -m mark --mark 0xACDA -j RETURN',
|
|
||||||
ip_version=4
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
# NAT floating IP addresses
|
|
||||||
for fip in external_network.floating_ips:
|
|
||||||
|
|
||||||
if fip.fixed_ip.version == fip.floating_ip.version:
|
|
||||||
rules.append(
|
|
||||||
Rule('-A PUBLIC_SNAT -s %s -j SNAT --to %s' % (
|
|
||||||
fip.fixed_ip,
|
|
||||||
fip.floating_ip
|
|
||||||
), ip_version=4)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add source NAT to handle NAT loopback case where external floating IP
|
|
||||||
# is used as the destination from internal endpoint
|
|
||||||
mgt_if = self.get_management_network(config).interface
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A PUBLIC_SNAT ! -o %s -j SNAT --to %s' % (
|
|
||||||
mgt_if.ifname,
|
|
||||||
str(external_network.interface.first_v4)
|
|
||||||
),
|
|
||||||
ip_version=4
|
|
||||||
))
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_mangle_table(self, config):
|
|
||||||
rules = [
|
|
||||||
Rule('*mangle', ip_version=4),
|
|
||||||
Rule(':INPUT - [0:0]', ip_version=4),
|
|
||||||
Rule(':OUTPUT - [0:0]', ip_version=4),
|
|
||||||
Rule(':FORWARD - [0:0]', ip_version=4),
|
|
||||||
Rule(':PREROUTING - [0:0]', ip_version=4),
|
|
||||||
Rule(':POSTROUTING - [0:0]', ip_version=4),
|
|
||||||
Rule(
|
|
||||||
('-A POSTROUTING -p udp -m udp --dport 68 '
|
|
||||||
'-j CHECKSUM --checksum-fill'),
|
|
||||||
ip_version=4),
|
|
||||||
Rule('COMMIT', ip_version=4)
|
|
||||||
]
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def _build_raw_table(self, config):
|
|
||||||
'''
|
|
||||||
Add raw rules (so we can mark private traffic and avoid NATing it)
|
|
||||||
'''
|
|
||||||
rules = [
|
|
||||||
Rule('*raw', ip_version=4),
|
|
||||||
Rule(':INPUT - [0:0]', ip_version=4),
|
|
||||||
Rule(':OUTPUT - [0:0]', ip_version=4),
|
|
||||||
Rule(':FORWARD - [0:0]', ip_version=4),
|
|
||||||
Rule(':PREROUTING - [0:0]', ip_version=4)
|
|
||||||
]
|
|
||||||
|
|
||||||
# do not NAT traffic generated from within the appliance
|
|
||||||
rules.append(Rule('-A OUTPUT -j MARK --set-mark 0xACDA', ip_version=4))
|
|
||||||
|
|
||||||
ext_net = self.get_external_network(config)
|
|
||||||
if ext_net:
|
|
||||||
ext_if = ext_net.interface
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A PREROUTING -i %s -j MARK --set-mark 0xACDA' %
|
|
||||||
ext_if.ifname, ip_version=4
|
|
||||||
))
|
|
||||||
|
|
||||||
for network in self.networks_by_type(config, Network.TYPE_INTERNAL):
|
|
||||||
if network.interface.first_v4:
|
|
||||||
address = sorted(
|
|
||||||
str(a) for a in network.interface.addresses
|
|
||||||
if a.version == 4
|
|
||||||
)[0]
|
|
||||||
rules.append(Rule(
|
|
||||||
'-A PREROUTING -d %s -j MARK --set-mark 0xACDA' % address,
|
|
||||||
ip_version=4
|
|
||||||
))
|
|
||||||
|
|
||||||
rules.append(Rule(':POSTROUTING - [0:0]', ip_version=4))
|
|
||||||
rules.append(Rule('COMMIT', ip_version=4))
|
|
||||||
return rules
|
|
@ -1,46 +0,0 @@
|
|||||||
vrrp_sync_group astara_vrrp_group {
|
|
||||||
group {
|
|
||||||
{%- for instance in vrrp_instances %}
|
|
||||||
{{ instance.name }}
|
|
||||||
{%- endfor %}
|
|
||||||
}
|
|
||||||
notify_master "{{ notify_script }} primary"
|
|
||||||
notify_backup "{{ notify_script }} backup"
|
|
||||||
notify_fault "{{ notify_script }} fault"
|
|
||||||
}
|
|
||||||
|
|
||||||
{%- for instance in vrrp_instances %}
|
|
||||||
vrrp_instance {{ instance.name }} {
|
|
||||||
native_ipv6
|
|
||||||
state {{ instance.state }}
|
|
||||||
interface {{ instance.interface }}
|
|
||||||
virtual_router_id {{ instance.vrrp_id }}
|
|
||||||
priority {{ priority }}
|
|
||||||
garp_master_delay {{ instance.garp_master_delay }}
|
|
||||||
unicast_src_ip {{ instance.unicast_src_ip }}
|
|
||||||
unicast_peer {
|
|
||||||
{%- for peer in peers %}
|
|
||||||
{{ peer }}
|
|
||||||
{%- endfor %}
|
|
||||||
}
|
|
||||||
{%- if instance.vips %}
|
|
||||||
virtual_ipaddress {
|
|
||||||
{{ instance.vips[0].address }} dev {{ instance.vips[0].interface }}
|
|
||||||
}
|
|
||||||
virtual_ipaddress_excluded {
|
|
||||||
{%- for vip in instance.vips[1:] %}
|
|
||||||
{{ vip.address }} dev {{ vip.interface }}
|
|
||||||
{%- endfor %}
|
|
||||||
}
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{%- if instance.routes %}
|
|
||||||
virtual_routes {
|
|
||||||
{%- for route in instance.routes %}
|
|
||||||
{{ route.destination }} via {{ route.gateway }} dev {{ instance.interface }}
|
|
||||||
{%- endfor %}
|
|
||||||
}
|
|
||||||
{%- endif %}
|
|
||||||
}
|
|
||||||
|
|
||||||
{%- endfor %}
|
|
@ -1,158 +0,0 @@
|
|||||||
# Copyright (c) 2016 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from astara_router.drivers import base, conntrackd
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
class KeepalivedVipAddress(object):
|
|
||||||
"""A virtual address entry of a keepalived configuration."""
|
|
||||||
|
|
||||||
def __init__(self, address, interface):
|
|
||||||
self.address = address
|
|
||||||
self.interface = interface
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (isinstance(other, KeepalivedVipAddress) and
|
|
||||||
self.address.ip == other.address.ip)
|
|
||||||
|
|
||||||
|
|
||||||
class KeepalivedRoute(object):
|
|
||||||
"""A virtual route entry in keepalived instance configuration"""
|
|
||||||
def __init__(self, destination, gateway):
|
|
||||||
self.destination = destination
|
|
||||||
self.gateway = gateway
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (
|
|
||||||
isinstance(other, KeepalivedRoute) and
|
|
||||||
(self.destination, self.gateway) ==
|
|
||||||
(other.destination, other.gateway)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class KeepalivedInstance(object):
|
|
||||||
def __init__(self, interface, unicast_src_ip, vrrp_id, state='BACKUP',
|
|
||||||
garp_master_delay=60):
|
|
||||||
self.interface = interface
|
|
||||||
self.vrrp_id = vrrp_id
|
|
||||||
self.unicast_src_ip = unicast_src_ip
|
|
||||||
self.name = 'astara_vrrp_' + interface
|
|
||||||
self.state = state
|
|
||||||
self.garp_master_delay = 60
|
|
||||||
self.vips = []
|
|
||||||
self.routes = []
|
|
||||||
|
|
||||||
def add_vip(self, address):
|
|
||||||
vip = KeepalivedVipAddress(address, self.interface)
|
|
||||||
if vip not in self.vips:
|
|
||||||
self.vips.append(vip)
|
|
||||||
|
|
||||||
def add_route(self, destination, gateway):
|
|
||||||
route = KeepalivedRoute(destination, gateway)
|
|
||||||
if route not in self.routes:
|
|
||||||
self.routes.append(route)
|
|
||||||
|
|
||||||
|
|
||||||
class KeepalivedManager(base.Manager):
|
|
||||||
CONFIG_FILE_TEMPLATE = os.path.join(
|
|
||||||
os.path.dirname(__file__), 'keepalived.conf.template')
|
|
||||||
|
|
||||||
# Debian defaults
|
|
||||||
CONFIG_FILE = '/etc/keepalived/keepalived.conf'
|
|
||||||
PID_FILE = '/var/run/keepalived.pid'
|
|
||||||
|
|
||||||
EXECUTABLE = 'service'
|
|
||||||
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
super(KeepalivedManager, self).__init__(root_helper)
|
|
||||||
self.instances = {}
|
|
||||||
self.unicast_src_ip = None
|
|
||||||
self.config_tmpl = utils.load_template(self.CONFIG_FILE_TEMPLATE)
|
|
||||||
self.peers = []
|
|
||||||
self.priority = 0
|
|
||||||
self.notify_script = conntrackd.ConntrackdManager.NOTIFY_SCRIPT
|
|
||||||
self._last_config_hash = None
|
|
||||||
|
|
||||||
def set_management_address(self, address):
|
|
||||||
"""Specify the address used for keepalived cluster communication"""
|
|
||||||
self.unicast_src_ip = address
|
|
||||||
for instance in self.instances.values():
|
|
||||||
instance.unicast_src_ip = address
|
|
||||||
|
|
||||||
def _get_instance(self, interface):
|
|
||||||
if interface in self.instances:
|
|
||||||
return self.instances[interface]
|
|
||||||
|
|
||||||
vrrp_id = len(self.instances) + 1
|
|
||||||
self.instances[interface] = KeepalivedInstance(
|
|
||||||
interface, self.unicast_src_ip, vrrp_id=vrrp_id)
|
|
||||||
return self.instances[interface]
|
|
||||||
|
|
||||||
def _is_running(self):
|
|
||||||
if not os.path.isfile(self.PID_FILE):
|
|
||||||
return False
|
|
||||||
|
|
||||||
pid = open(self.PID_FILE).read().strip()
|
|
||||||
proc_cmd = os.path.join('/proc', pid, 'cmdline')
|
|
||||||
if not os.path.isfile(proc_cmd):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if 'keepalived' not in open(proc_cmd).read():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def add_vrrp_instance(self, interface, addresses):
|
|
||||||
instance = self._get_instance(interface)
|
|
||||||
[instance.add_vip(addr) for addr in addresses]
|
|
||||||
|
|
||||||
def config(self):
|
|
||||||
return self.config_tmpl.render(
|
|
||||||
priority=self.priority,
|
|
||||||
peers=self.peers,
|
|
||||||
notify_script=self.notify_script,
|
|
||||||
vrrp_instances=self.instances.values())
|
|
||||||
|
|
||||||
def reload(self):
|
|
||||||
try:
|
|
||||||
last_config_hash = utils.hash_file(self.CONFIG_FILE)
|
|
||||||
except IOError:
|
|
||||||
last_config_hash = None
|
|
||||||
|
|
||||||
utils.replace_file('/tmp/keepalived.conf', self.config())
|
|
||||||
utils.execute(
|
|
||||||
['mv', '/tmp/keepalived.conf', '/etc/keepalived/keepalived.conf'],
|
|
||||||
self.root_helper)
|
|
||||||
|
|
||||||
if utils.hash_file(self.CONFIG_FILE) == last_config_hash:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._is_running():
|
|
||||||
self.sudo('keepalived', 'reload')
|
|
||||||
else:
|
|
||||||
self.sudo('keepalived', 'restart')
|
|
||||||
|
|
||||||
def set_default_gateway(self, ip_version, gateway_ip, interface):
|
|
||||||
instance = self._get_instance(interface)
|
|
||||||
if ip_version == 6:
|
|
||||||
default = 'default6'
|
|
||||||
else:
|
|
||||||
default = 'default'
|
|
||||||
instance.add_route(default, gateway_ip)
|
|
||||||
|
|
||||||
def set_priority(self, priority):
|
|
||||||
self.priority = priority
|
|
@ -1,37 +0,0 @@
|
|||||||
# Copyright (c) 2015 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from astara_router.drivers.loadbalancer import nginx
|
|
||||||
|
|
||||||
# XXX move to config
|
|
||||||
CONFIGURED_LB_DRIVER = 'nginx'
|
|
||||||
|
|
||||||
AVAILABLE_DRIVERS = {
|
|
||||||
'nginx': nginx.NginxLB,
|
|
||||||
'nginx+': nginx.NginxPlusLB,
|
|
||||||
# 'haxproxy': HaProxyLB,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDriverException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_loadbalancer_driver(name):
|
|
||||||
try:
|
|
||||||
return AVAILABLE_DRIVERS[name]
|
|
||||||
except KeyError:
|
|
||||||
raise InvalidDriverException(
|
|
||||||
'Could not find LB driver by name %s' % name)
|
|
@ -1,19 +0,0 @@
|
|||||||
{%- for listener in loadbalancer.listeners %}
|
|
||||||
{%- if listener.default_pool and listener.default_pool.members %}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen {{ loadbalancer.vip_address }}:{{ listener.protocol_port }};
|
|
||||||
location / {
|
|
||||||
proxy_pass {{ listener.protocol.lower() }}://pool_{{ listener.default_pool.id }};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream pool_{{ listener.default_pool.id }} {
|
|
||||||
{%- for member in listener.default_pool.members: %}
|
|
||||||
server {{ member.address }}:{{ member.protocol_port }} weight={{ member.weight }};
|
|
||||||
{%- endfor %}
|
|
||||||
}
|
|
||||||
|
|
||||||
{%- endif %}
|
|
||||||
{%- endfor %}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
|||||||
# Copyright (c) 2015 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router.utils import execute, load_template
|
|
||||||
|
|
||||||
|
|
||||||
class NginxLB(base.Manager):
|
|
||||||
NAME = 'nginx'
|
|
||||||
CONFIG_PATH = '/etc/nginx/sites-enabled/'
|
|
||||||
CONFIG_FILE_TEMPLATE = os.path.join(
|
|
||||||
os.path.dirname(__file__), 'nginx.conf.template')
|
|
||||||
INIT = 'nginx'
|
|
||||||
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes NginxLB class.
|
|
||||||
|
|
||||||
:type root_helper: str
|
|
||||||
:param root_helper: System utility used to gain escalate privileges.
|
|
||||||
"""
|
|
||||||
super(NginxLB, self).__init__(root_helper)
|
|
||||||
self.config_tmpl = load_template(self.CONFIG_FILE_TEMPLATE)
|
|
||||||
|
|
||||||
def _render_config_template(self, path, config):
|
|
||||||
with open(path, 'w') as out:
|
|
||||||
out.write(
|
|
||||||
self.config_tmpl.render(loadbalancer=config)
|
|
||||||
)
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
execute(['service', self.INIT, 'restart'], self.root_helper)
|
|
||||||
|
|
||||||
def update_config(self, config):
|
|
||||||
path = os.path.join(
|
|
||||||
self.CONFIG_PATH, 'ak-loadbalancer-%s.conf' % config.id)
|
|
||||||
self._render_config_template(path=path, config=config)
|
|
||||||
self.restart()
|
|
||||||
|
|
||||||
|
|
||||||
class NginxPlusLB(NginxLB):
|
|
||||||
NAME = 'nginxplus'
|
|
||||||
CONFIG_FILE = '/tmp/nginx_plus.conf'
|
|
||||||
INIT = 'nginxplus'
|
|
@ -1,136 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from astara_router.settings import internal_metadata_port
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router.utils import execute, replace_file
|
|
||||||
|
|
||||||
|
|
||||||
CONF_PATH = '/etc/metadata.conf'
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataManager(base.Manager):
|
|
||||||
"""
|
|
||||||
A class to provide facilities to interact with the Nova metadata service.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes MetataManager class.
|
|
||||||
|
|
||||||
:type root_helper: str
|
|
||||||
:param root_helper: System utility used to gain escalate privileges.
|
|
||||||
"""
|
|
||||||
super(MetadataManager, self).__init__(root_helper)
|
|
||||||
|
|
||||||
def should_restart(self, config):
|
|
||||||
"""
|
|
||||||
This function determines if the networks have changed since <config>
|
|
||||||
was initialized.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config: An astara_router.models.Configuration object containing
|
|
||||||
the current configuration of the system's networks.
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
net_ids = set(
|
|
||||||
[net.id for net in config.networks if net.is_tenant_network]
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
config_dict = json.load(open(CONF_PATH))
|
|
||||||
except:
|
|
||||||
# If we can't read the file, assume networks were added/removed
|
|
||||||
return True
|
|
||||||
|
|
||||||
orchestrator_addr = config_dict.get('orchestrator_metadata_address')
|
|
||||||
orchestrator_port = config_dict.get('orchestrator_metadata_port')
|
|
||||||
|
|
||||||
return (
|
|
||||||
net_ids != set(config_dict.get('networks', {}).keys()) or
|
|
||||||
orchestrator_addr != config.metadata_address or
|
|
||||||
orchestrator_port != config.metadata_port)
|
|
||||||
|
|
||||||
def save_config(self, config):
|
|
||||||
"""
|
|
||||||
Writes <config> to the metadata configuration file (<CONF_PATH>).
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config: An astara_router.models.Configuration object containing
|
|
||||||
the configuration of metadata service.
|
|
||||||
"""
|
|
||||||
config_data = build_config(config)
|
|
||||||
|
|
||||||
replace_file(
|
|
||||||
'/tmp/metadata.conf',
|
|
||||||
json.dumps(config_data, sort_keys=True)
|
|
||||||
)
|
|
||||||
execute(['mv', '/tmp/metadata.conf', CONF_PATH], self.root_helper)
|
|
||||||
|
|
||||||
def ensure_started(self):
|
|
||||||
"""
|
|
||||||
Checks if the metadata service is started and starts it if it is
|
|
||||||
determined to be stopped.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
execute(['service', 'metadata', 'status'], self.root_helper)
|
|
||||||
except:
|
|
||||||
execute(['service', 'metadata', 'start'], self.root_helper)
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""
|
|
||||||
Restarts the metadata service using the init script.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
execute(['service', 'metadata', 'stop'], self.root_helper)
|
|
||||||
except:
|
|
||||||
# failure is ok here
|
|
||||||
pass
|
|
||||||
execute(['service', 'metadata', 'start'], self.root_helper)
|
|
||||||
|
|
||||||
|
|
||||||
def build_config(config):
|
|
||||||
"""
|
|
||||||
Determines the configuration of the metadata service.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
:param config:
|
|
||||||
:rtype: astara_router.models.Configuration
|
|
||||||
"""
|
|
||||||
network_data = {}
|
|
||||||
|
|
||||||
for net in config.networks:
|
|
||||||
if not net.is_tenant_network:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ip_instance_map = {}
|
|
||||||
for a in net.address_allocations:
|
|
||||||
for ip in a.ip_addresses:
|
|
||||||
ip_instance_map[ip] = a.device_id
|
|
||||||
|
|
||||||
network_data[net.id] = {
|
|
||||||
'listen_port': internal_metadata_port(net.interface.ifname),
|
|
||||||
'ip_instance_map': ip_instance_map
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'tenant_id': config.tenant_id,
|
|
||||||
'orchestrator_metadata_address': config.metadata_address,
|
|
||||||
'orchestrator_metadata_port': config.metadata_port,
|
|
||||||
'networks': network_data,
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import netaddr
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
|
|
||||||
class PingManager(base.Manager):
|
|
||||||
"""
|
|
||||||
A class which provide a facade to the system ping utility. Supports both
|
|
||||||
IPv4 and IPv6.
|
|
||||||
"""
|
|
||||||
|
|
||||||
exe_map = {
|
|
||||||
4: 'ping',
|
|
||||||
6: 'ping6'
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes PingManager class.
|
|
||||||
|
|
||||||
:type root_helper: str
|
|
||||||
:param root_helper: System utility to escalate privileges.
|
|
||||||
"""
|
|
||||||
super(PingManager, self).__init__(root_helper)
|
|
||||||
|
|
||||||
def do(self, ip):
|
|
||||||
"""
|
|
||||||
Sends a single ICMP packet to <ip> using the systems ping utility.
|
|
||||||
|
|
||||||
:type ip: str
|
|
||||||
:param ip: The IP address to send ICMP packets to.
|
|
||||||
:rtype: bool. If <ip> responds to the ICMP packet, returns True else,
|
|
||||||
returns False
|
|
||||||
"""
|
|
||||||
version = netaddr.IPAddress(ip).version
|
|
||||||
args = ['-c', '1', ip]
|
|
||||||
try:
|
|
||||||
utils.execute([self.exe_map.get(version)] + args)
|
|
||||||
return True
|
|
||||||
except RuntimeError:
|
|
||||||
return False
|
|
@ -1,105 +0,0 @@
|
|||||||
# Copyright 2016 Akanda, Inc
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
import jinja2
|
|
||||||
|
|
||||||
from astara_router.drivers import base
|
|
||||||
from astara_router import utils
|
|
||||||
|
|
||||||
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'templates')
|
|
||||||
|
|
||||||
|
|
||||||
STRONGSWAN_TRANSLATIONS = {
|
|
||||||
"3des": "3des",
|
|
||||||
"aes-128": "aes128",
|
|
||||||
"aes-256": "aes256",
|
|
||||||
"aes-192": "aes192",
|
|
||||||
"group2": "modp1024",
|
|
||||||
"group5": "modp1536",
|
|
||||||
"group14": "modp2048",
|
|
||||||
"group15": "modp3072",
|
|
||||||
"bi-directional": "start",
|
|
||||||
"response-only": "add",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StrongswanManager(base.Manager):
|
|
||||||
"""
|
|
||||||
A class to interact with strongswan, an IPSEC VPN daemon.
|
|
||||||
"""
|
|
||||||
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
|
|
||||||
"""
|
|
||||||
Initializes StrongswanManager class.
|
|
||||||
|
|
||||||
:type root_helper: string
|
|
||||||
:param root_helper: System utility used to gain escalate privileges.
|
|
||||||
"""
|
|
||||||
super(StrongswanManager, self).__init__(root_helper)
|
|
||||||
|
|
||||||
def save_config(self, config):
|
|
||||||
"""
|
|
||||||
Writes config file for strongswan daemon.
|
|
||||||
|
|
||||||
:type config: astara_router.models.Configuration
|
|
||||||
"""
|
|
||||||
|
|
||||||
env = jinja2.Environment(
|
|
||||||
loader=jinja2.FileSystemLoader(TEMPLATE_DIR)
|
|
||||||
)
|
|
||||||
|
|
||||||
env.filters['strongswan'] = lambda v: STRONGSWAN_TRANSLATIONS.get(v, v)
|
|
||||||
|
|
||||||
templates = ('ipsec.conf', 'ipsec.secrets')
|
|
||||||
|
|
||||||
for template_name in templates:
|
|
||||||
tmpl = env.get_template(template_name+'.j2')
|
|
||||||
|
|
||||||
tmp = os.path.join('/tmp', template_name)
|
|
||||||
utils.replace_file(tmp, tmpl.render(vpnservices=config.vpn))
|
|
||||||
|
|
||||||
for template_name in templates:
|
|
||||||
tmp = os.path.join('/tmp', template_name)
|
|
||||||
etc = os.path.join('/etc', template_name)
|
|
||||||
utils.execute(['mv', tmp, etc], self.root_helper)
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""
|
|
||||||
Restart the Strongswan daemon using the system provided init scripts.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
utils.execute(
|
|
||||||
['service', 'strongswan', 'status'],
|
|
||||||
self.root_helper
|
|
||||||
)
|
|
||||||
except: # pragma no cover
|
|
||||||
utils.execute(['service', 'strongswan', 'start'], self.root_helper)
|
|
||||||
else: # pragma no cover
|
|
||||||
utils.execute(
|
|
||||||
['service', 'strongswan', 'reload'],
|
|
||||||
self.root_helper
|
|
||||||
)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
Stop the Strongswan daemon using the system provided init scripts.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
utils.execute(
|
|
||||||
['service', 'strongswan', 'stop'],
|
|
||||||
self.root_helper
|
|
||||||
)
|
|
||||||
except: # pragma no cover
|
|
||||||
pass
|
|
@ -1,3 +0,0 @@
|
|||||||
These templates were originally copied from the upstream neutron-vpaas [1].
|
|
||||||
|
|
||||||
[1] http://git.openstack.org/cgit/openstack/neutron-vpnaas/tree/neutron_vpnaas/services/vpn/device_drivers/template/strongswan
|
|
@ -1,39 +0,0 @@
|
|||||||
config setup
|
|
||||||
|
|
||||||
conn %default
|
|
||||||
ikelifetime=60m
|
|
||||||
keylife=20m
|
|
||||||
rekeymargin=3m
|
|
||||||
keyingtries=1
|
|
||||||
mobike=no
|
|
||||||
{% for vpnservice in vpnservices %}
|
|
||||||
# Configuration for {{vpnservice.name}}
|
|
||||||
{% for ipsec_site_connection in vpnservice.ipsec_site_connections%}
|
|
||||||
conn {{ipsec_site_connection.id}}
|
|
||||||
authby=secret
|
|
||||||
keyexchange=ike{{ipsec_site_connection.ikepolicy.ike_version}}
|
|
||||||
left={{vpnservice.get_external_ip(ipsec_site_connection.peer_address)}}
|
|
||||||
leftsubnet={{ipsec_site_connection.local_ep_group.cidrs|join(',')}}
|
|
||||||
leftid={{vpnservice.get_external_ip(ipsec_site_connection.peer_address)}}
|
|
||||||
leftfirewall=yes
|
|
||||||
right={{ipsec_site_connection.peer_address}}
|
|
||||||
rightsubnet={{ipsec_site_connection.peer_ep_group.cidrs|join(',')}}
|
|
||||||
rightid={{ipsec_site_connection.peer_id}}
|
|
||||||
auto=route
|
|
||||||
dpdaction={{ipsec_site_connection.dpd.action}}
|
|
||||||
dpddelay={{ipsec_site_connection.dpd.interval}}
|
|
||||||
dpdtimeout={{ipsec_site_connection.dpd.timeout}}
|
|
||||||
|
|
||||||
# ike
|
|
||||||
ike={{ipsec_site_connection.ikepolicy.encryption_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.auth_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.pfs|strongswan}}
|
|
||||||
ikelifetime={{ipsec_site_connection.ikepolicy.lifetime.value}}s
|
|
||||||
|
|
||||||
# ipsec
|
|
||||||
{{ipsec_site_connection.ipsecpolicy.transform_protocol}}={{ipsec_site_connection.ikepolicy.encryption_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.auth_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.pfs|strongswan}}
|
|
||||||
lifetime={{ipsec_site_connection.ipsecpolicy.lifetime.value}}s
|
|
||||||
|
|
||||||
type={{ipsec_site_connection.ipsecpolicy.encapsulation_mode}}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
{% for vpnservice in vpnservices %}
|
|
||||||
# Configuration for {{vpnservice.name}}
|
|
||||||
{% for ipsec_site_connection in vpnservice.ipsec_site_connections %}
|
|
||||||
{{ipsec_site_connection.external_ip}} {{ipsec_site_connection.peer_id}} : PSK "{{ipsec_site_connection.psk}}"
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
@ -1,329 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from astara_router import models
|
|
||||||
from astara_router import settings
|
|
||||||
from astara_router.drivers import (bird, conntrackd, dnsmasq, ip, metadata,
|
|
||||||
iptables, arp, hostname, loadbalancer)
|
|
||||||
from astara_router.drivers.vpn import ipsec
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceManagerBase(object):
|
|
||||||
def __init__(self, state_path='.'):
|
|
||||||
self._config = None
|
|
||||||
self.state_path = os.path.abspath(state_path)
|
|
||||||
self._vrrp_ip_mgr = None
|
|
||||||
self._reload_callbacks = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ip_mgr(self):
|
|
||||||
ip_mgr = ip.IPManager()
|
|
||||||
ip_mgr.ensure_mapping()
|
|
||||||
|
|
||||||
if not self._config:
|
|
||||||
# we do not yet have config, so use standard ip manager for
|
|
||||||
# ensuring initial intrefaces
|
|
||||||
return ip_mgr
|
|
||||||
if self._config and self._config.ha:
|
|
||||||
if not self._vrrp_ip_mgr:
|
|
||||||
self._vrrp_ip_mgr = ip.VRRPIPManager()
|
|
||||||
self._reload_callbacks.append(self._vrrp_ip_mgr.reload)
|
|
||||||
|
|
||||||
# peers and prio can change and be updated via config, need to
|
|
||||||
# ensure the vrrp manager is up to date every access.
|
|
||||||
self._vrrp_ip_mgr.set_peers(
|
|
||||||
self._config.ha_config.get('peers', []))
|
|
||||||
self._vrrp_ip_mgr.set_priority(
|
|
||||||
self._config.ha_config.get('priority', 0))
|
|
||||||
|
|
||||||
return self._vrrp_ip_mgr
|
|
||||||
else:
|
|
||||||
# we may not yet have config, so use standard ip manager for
|
|
||||||
# ensuring initial interfaces
|
|
||||||
return ip_mgr
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self):
|
|
||||||
"""Make config a read-only property.
|
|
||||||
|
|
||||||
To update the value, update_config() must called to change the global
|
|
||||||
state of appliance.
|
|
||||||
"""
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
def update_config(self, config, cache):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update_interfaces(self):
|
|
||||||
if self._config is None:
|
|
||||||
return
|
|
||||||
for network in self._config.networks:
|
|
||||||
self.ip_mgr.disable_duplicate_address_detection(network)
|
|
||||||
|
|
||||||
self.ip_mgr.update_interfaces(self._config.interfaces)
|
|
||||||
|
|
||||||
def reload_config(self):
|
|
||||||
"""Calls any post-config reload callbacks to reload services
|
|
||||||
|
|
||||||
Required for things like keepalived, which gets its config built
|
|
||||||
by multiple drivers, in order to avoid unncessary restarts.
|
|
||||||
"""
|
|
||||||
[cb() for cb in self._reload_callbacks]
|
|
||||||
|
|
||||||
|
|
||||||
class SystemManager(ServiceManagerBase):
|
|
||||||
def __init__(self, state_path='.'):
|
|
||||||
super(SystemManager, self).__init__(state_path)
|
|
||||||
self._config = models.SystemConfiguration()
|
|
||||||
|
|
||||||
def update_config(self, config, cache):
|
|
||||||
self._config = config
|
|
||||||
self.update_hostname()
|
|
||||||
self.update_interfaces()
|
|
||||||
|
|
||||||
def update_hostname(self):
|
|
||||||
mgr = hostname.HostnameManager()
|
|
||||||
mgr.update(self._config)
|
|
||||||
|
|
||||||
|
|
||||||
class RouterManager(ServiceManagerBase):
|
|
||||||
|
|
||||||
def update_config(self, config, cache):
|
|
||||||
self._config = config
|
|
||||||
self.update_interfaces()
|
|
||||||
self.update_dhcp()
|
|
||||||
self.update_metadata()
|
|
||||||
self.update_bgp_and_radv()
|
|
||||||
self.update_firewall()
|
|
||||||
self.update_routes(cache)
|
|
||||||
self.update_arp()
|
|
||||||
self.update_conntrackd()
|
|
||||||
self.update_ipsec_vpn()
|
|
||||||
self.reload_config()
|
|
||||||
|
|
||||||
def update_conntrackd(self):
|
|
||||||
if not self._config.ha:
|
|
||||||
return
|
|
||||||
mgr = conntrackd.ConntrackdManager()
|
|
||||||
mgr.save_config(self._config, self.ip_mgr.generic_to_host)
|
|
||||||
mgr.restart()
|
|
||||||
|
|
||||||
def update_dhcp(self):
|
|
||||||
mgr = dnsmasq.DHCPManager()
|
|
||||||
mgr.delete_all_config()
|
|
||||||
for network in self._config.networks:
|
|
||||||
real_ifname = self.ip_mgr.generic_to_host(network.interface.ifname)
|
|
||||||
mgr.update_network_dhcp_config(real_ifname, network)
|
|
||||||
mgr.restart()
|
|
||||||
|
|
||||||
def update_metadata(self):
|
|
||||||
mgr = metadata.MetadataManager()
|
|
||||||
should_restart = mgr.should_restart(self._config)
|
|
||||||
mgr.save_config(self._config)
|
|
||||||
if should_restart:
|
|
||||||
mgr.restart()
|
|
||||||
else:
|
|
||||||
mgr.ensure_started()
|
|
||||||
|
|
||||||
def update_bgp_and_radv(self):
|
|
||||||
mgr = bird.BirdManager()
|
|
||||||
mgr.save_config(self._config, self.ip_mgr.generic_mapping)
|
|
||||||
mgr.restart()
|
|
||||||
|
|
||||||
def update_firewall(self):
|
|
||||||
mgr = iptables.IPTablesManager()
|
|
||||||
mgr.save_config(self._config, self.ip_mgr.generic_mapping)
|
|
||||||
mgr.restart()
|
|
||||||
|
|
||||||
def update_routes(self, cache):
|
|
||||||
self.ip_mgr.update_default_gateway(self._config)
|
|
||||||
self.ip_mgr.update_host_routes(self._config, cache)
|
|
||||||
|
|
||||||
def update_arp(self):
|
|
||||||
mgr = arp.ARPManager()
|
|
||||||
mgr.send_gratuitous_arp_for_floating_ips(
|
|
||||||
self._config,
|
|
||||||
self.ip_mgr.generic_to_host
|
|
||||||
)
|
|
||||||
mgr.remove_stale_entries(self._config)
|
|
||||||
|
|
||||||
def update_ipsec_vpn(self):
|
|
||||||
mgr = ipsec.StrongswanManager()
|
|
||||||
|
|
||||||
if self._config.vpn:
|
|
||||||
mgr.save_config(self._config)
|
|
||||||
mgr.restart()
|
|
||||||
else:
|
|
||||||
mgr.stop()
|
|
||||||
|
|
||||||
def get_interfaces(self):
|
|
||||||
return self.ip_mgr.get_interfaces()
|
|
||||||
|
|
||||||
def get_interface(self, ifname):
|
|
||||||
return self.ip_mgr.get_interface(ifname)
|
|
||||||
|
|
||||||
def _map_virtual_to_real_interfaces(self, virt_data):
|
|
||||||
rules = []
|
|
||||||
|
|
||||||
rules.extend(
|
|
||||||
'%s = "%s"' % i for i in self.ip_mgr.generic_mapping.items()
|
|
||||||
)
|
|
||||||
|
|
||||||
rules.append(re.sub('([\s!])(ge\d+([\s:]|$))', r'\1$\2', virt_data))
|
|
||||||
return '\n'.join(rules)
|
|
||||||
|
|
||||||
def get_config_or_default(self):
|
|
||||||
# This is a hack to provide compatability with the original API, see
|
|
||||||
# Manager.config()
|
|
||||||
if not self._config:
|
|
||||||
return models.RouterConfiguration()
|
|
||||||
else:
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
|
|
||||||
class LoadBalancerManager(ServiceManagerBase):
|
|
||||||
def __init__(self, state_path='.'):
|
|
||||||
super(LoadBalancerManager, self).__init__(state_path)
|
|
||||||
self.lb_manager = loadbalancer.get_loadbalancer_driver(
|
|
||||||
# xxx pull from cfg
|
|
||||||
loadbalancer.CONFIGURED_LB_DRIVER)()
|
|
||||||
|
|
||||||
def update_config(self, config, cache):
|
|
||||||
self._config = config
|
|
||||||
self.lb_manager.update_config(self.config)
|
|
||||||
|
|
||||||
|
|
||||||
SERVICE_MANAGER_MAP = {
|
|
||||||
'router': RouterManager,
|
|
||||||
'loadbalancer': LoadBalancerManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Manager(object):
|
|
||||||
def __init__(self, state_path='.'):
|
|
||||||
self.state_path = os.path.abspath(state_path)
|
|
||||||
self.ip_mgr = ip.IPManager()
|
|
||||||
self.ip_mgr.ensure_mapping()
|
|
||||||
|
|
||||||
# Holds the common system config
|
|
||||||
self._system_config = models.SystemConfiguration()
|
|
||||||
|
|
||||||
# Holds config models for various services (router, loadbalancer)
|
|
||||||
self._service_configs = []
|
|
||||||
|
|
||||||
self._service_managers = {
|
|
||||||
'system': SystemManager()
|
|
||||||
}
|
|
||||||
self._load_managers()
|
|
||||||
|
|
||||||
def _load_managers(self):
|
|
||||||
for svc in settings.ENABLED_SERVICES:
|
|
||||||
manager = SERVICE_MANAGER_MAP.get(svc)
|
|
||||||
if manager:
|
|
||||||
self._service_managers[svc] = manager()
|
|
||||||
|
|
||||||
def get_manager(self, service):
|
|
||||||
try:
|
|
||||||
return self._service_managers[service]
|
|
||||||
except:
|
|
||||||
raise Exception('No such service manager loaded for appliance '
|
|
||||||
'service %s' % service)
|
|
||||||
|
|
||||||
def management_address(self, ensure_configuration=False):
|
|
||||||
return self.ip_mgr.get_management_address(ensure_configuration)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def router(self):
|
|
||||||
"""Returns the router manager.
|
|
||||||
This is mostly to keep compat with the existing API.
|
|
||||||
"""
|
|
||||||
return self.get_manager('router')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def system_config(self):
|
|
||||||
"""Make config a read-only property.
|
|
||||||
|
|
||||||
To update the value, update_config() must called to change the global
|
|
||||||
state of appliance.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._system_config
|
|
||||||
|
|
||||||
@property
|
|
||||||
def service_configs(self):
|
|
||||||
"""Make config a read-only property.
|
|
||||||
|
|
||||||
To update the value, update_config() must called to change the global
|
|
||||||
state of router.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._service_configs
|
|
||||||
|
|
||||||
def update_config(self, system_config, service_configs, cache):
|
|
||||||
self._system_config = system_config
|
|
||||||
self._service_configs = service_configs
|
|
||||||
|
|
||||||
# first update the system config
|
|
||||||
manager = self.get_manager(self.system_config.service_name)
|
|
||||||
manager.update_config(self.system_config, cache)
|
|
||||||
|
|
||||||
for svc_cfg in self.service_configs:
|
|
||||||
manager = self.get_manager(svc_cfg.service_name)
|
|
||||||
manager.update_config(svc_cfg, cache)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self):
|
|
||||||
out = {}
|
|
||||||
if 'router' in self._service_managers:
|
|
||||||
# The original appliance API provides router config
|
|
||||||
# in the root 'configuration' key. We want to move that
|
|
||||||
# to the 'services' bucket but provide compat to those who might
|
|
||||||
# still be expecting it in the root. This seeds the root with the
|
|
||||||
# default empty values if no router is associated with the
|
|
||||||
# appliance and allows for
|
|
||||||
# ['configuration']['services']['router'] to be None at the same
|
|
||||||
# time.
|
|
||||||
router_cfg = self.router.get_config_or_default().to_dict()
|
|
||||||
out = router_cfg
|
|
||||||
else:
|
|
||||||
out = {}
|
|
||||||
|
|
||||||
out['services'] = {}
|
|
||||||
for svc in SERVICE_MANAGER_MAP:
|
|
||||||
try:
|
|
||||||
manager = self.get_manager(svc)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
out['services'][svc] = manager.config
|
|
||||||
|
|
||||||
out['system'] = self.system_config
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
class ManagerProxy(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.instance = None
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
if not self.instance:
|
|
||||||
self.instance = Manager()
|
|
||||||
return getattr(self.instance, name)
|
|
||||||
|
|
||||||
manager = ManagerProxy()
|
|
@ -1,190 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import atexit
|
|
||||||
import contextlib
|
|
||||||
import json
|
|
||||||
import functools
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import urlparse
|
|
||||||
|
|
||||||
import eventlet
|
|
||||||
import eventlet.wsgi
|
|
||||||
import requests
|
|
||||||
from werkzeug import exceptions
|
|
||||||
from werkzeug import wrappers
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkMetadataProxyHandler(object):
|
|
||||||
"""Proxy metadata request onto the RUG proxy
|
|
||||||
The proxy allows access resources that are not accessible within the
|
|
||||||
isolated tenant context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tenant_id, network_id, config_file):
|
|
||||||
self.tenant_id = tenant_id
|
|
||||||
self.network_id = network_id
|
|
||||||
self.config_file = config_file
|
|
||||||
self.config_mtime = 0
|
|
||||||
self._config_dict = {}
|
|
||||||
self._ip_instance_map = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config_dict(self):
|
|
||||||
config_mtime = os.stat(self.config_file).st_mtime
|
|
||||||
if config_mtime > self.config_mtime:
|
|
||||||
LOG.debug("Metadata proxy configuration has changed; reloading...")
|
|
||||||
self._config_dict = json.load(open(self.config_file))
|
|
||||||
self.config_mtime = config_mtime
|
|
||||||
return self._config_dict
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
request = wrappers.Request(environ)
|
|
||||||
|
|
||||||
LOG.debug("Request: %s", request)
|
|
||||||
try:
|
|
||||||
response = self._proxy_request(request.remote_addr,
|
|
||||||
request.path,
|
|
||||||
request.query_string)
|
|
||||||
except Exception:
|
|
||||||
LOG.exception("Unexpected error.")
|
|
||||||
msg = ('An unknown error has occurred. '
|
|
||||||
'Please try your request again.')
|
|
||||||
response = exceptions.InternalServerError(description=unicode(msg))
|
|
||||||
|
|
||||||
return response(environ, start_response)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ip_instance_map(self):
|
|
||||||
self._ip_instance_map = self.config_dict['networks'][
|
|
||||||
self.network_id]['ip_instance_map']
|
|
||||||
return self._ip_instance_map
|
|
||||||
|
|
||||||
@property
|
|
||||||
def orchestrator_loc(self):
|
|
||||||
addr = self.config_dict['orchestrator_metadata_address']
|
|
||||||
port = self.config_dict['orchestrator_metadata_port']
|
|
||||||
return '[%s]:%d' % (addr, port)
|
|
||||||
|
|
||||||
def _proxy_request(self, remote_address, path_info, query_string):
|
|
||||||
headers = {
|
|
||||||
'X-Forwarded-For': remote_address,
|
|
||||||
'X-Instance-ID': self.ip_instance_map.get(remote_address, ''),
|
|
||||||
'X-Quantum-Network-ID': self.network_id,
|
|
||||||
'X-Tenant-ID': self.tenant_id
|
|
||||||
}
|
|
||||||
|
|
||||||
url = urlparse.urlunsplit((
|
|
||||||
'http',
|
|
||||||
self.orchestrator_loc,
|
|
||||||
path_info,
|
|
||||||
query_string,
|
|
||||||
''))
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code == requests.codes.ok:
|
|
||||||
LOG.debug(response)
|
|
||||||
return wrappers.Response(response.content, mimetype='text/plain')
|
|
||||||
elif response.status_code == requests.codes.not_found:
|
|
||||||
return exceptions.NotFound()
|
|
||||||
elif response.status_code == requests.codes.internal_server_error:
|
|
||||||
msg = 'Remote metadata server experienced an error.'
|
|
||||||
return exceptions.InternalServerError(description=unicode(msg))
|
|
||||||
else:
|
|
||||||
raise Exception('Unexpected response code: %s' % response.status)
|
|
||||||
|
|
||||||
|
|
||||||
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
|
||||||
"""Daemonize process by doing Stevens double fork."""
|
|
||||||
# fork first time
|
|
||||||
_fork()
|
|
||||||
|
|
||||||
# decouple from parent environment
|
|
||||||
os.chdir("/")
|
|
||||||
os.setsid()
|
|
||||||
os.umask(0)
|
|
||||||
|
|
||||||
# fork second time
|
|
||||||
_fork()
|
|
||||||
|
|
||||||
# redirect standard file descriptors
|
|
||||||
sys.stdout.flush()
|
|
||||||
sys.stderr.flush()
|
|
||||||
stdin = file(stdin, 'r')
|
|
||||||
stdout = file(stdout, 'a+')
|
|
||||||
stderr = file(stderr, 'a+', 0)
|
|
||||||
os.dup2(stdin.fileno(), sys.stdin.fileno())
|
|
||||||
os.dup2(stdout.fileno(), sys.stdout.fileno())
|
|
||||||
os.dup2(stderr.fileno(), sys.stderr.fileno())
|
|
||||||
|
|
||||||
# write a pidfile
|
|
||||||
pidfile = '/var/run/metadata.pid'
|
|
||||||
atexit.register(functools.partial(os.remove, pidfile))
|
|
||||||
pid = str(os.getpid())
|
|
||||||
with contextlib.closing(open(pidfile, 'w+')) as f:
|
|
||||||
f.write("%s\n" % pid)
|
|
||||||
|
|
||||||
|
|
||||||
def _fork():
|
|
||||||
try:
|
|
||||||
pid = os.fork()
|
|
||||||
if pid > 0:
|
|
||||||
sys.exit(0)
|
|
||||||
except OSError, e:
|
|
||||||
sys.stderr.write("fork failed %d (%s)\n" % (e.errno, e.strerror))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
eventlet.monkey_patch()
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-D", "--no-daemon", help="don't daemonize",
|
|
||||||
action="store_false", dest='daemonize', default=True)
|
|
||||||
parser.add_argument("config_file", help="Proxy configuration file")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
config_dict = json.load(open(args.config_file))
|
|
||||||
except IOError:
|
|
||||||
raise SystemError('Unable to open config file at %s.' %
|
|
||||||
args.config_file)
|
|
||||||
except:
|
|
||||||
raise SystemError('Unable to parse config file at %s.' %
|
|
||||||
args.config_file)
|
|
||||||
|
|
||||||
if args.daemonize:
|
|
||||||
daemonize()
|
|
||||||
|
|
||||||
pool = eventlet.GreenPool(1000)
|
|
||||||
|
|
||||||
tenant_id = config_dict.pop('tenant_id')
|
|
||||||
for network_id, config in config_dict['networks'].items():
|
|
||||||
app = NetworkMetadataProxyHandler(tenant_id,
|
|
||||||
network_id,
|
|
||||||
args.config_file)
|
|
||||||
socket = eventlet.listen(('0.0.0.0', config['listen_port']),
|
|
||||||
backlog=128)
|
|
||||||
pool.spawn_n(eventlet.wsgi.server, socket, app, custom_pool=pool)
|
|
||||||
|
|
||||||
pool.waitall()
|
|
File diff suppressed because it is too large
Load Diff
@ -1,10 +0,0 @@
|
|||||||
from astara_router.defaults import * # noqa
|
|
||||||
|
|
||||||
# If astara_local_settings.py is located in your python path,
|
|
||||||
# it can be used to override the defaults. DIB will install this
|
|
||||||
# into /usr/local/share/astara and append that path to the gunicorn's
|
|
||||||
# python path.
|
|
||||||
try:
|
|
||||||
from astara_local_settings import * # noqa
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
@ -1,126 +0,0 @@
|
|||||||
# Copyright 2014 DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Author: DreamHost, LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import flask
|
|
||||||
import jinja2
|
|
||||||
import netaddr
|
|
||||||
|
|
||||||
from astara_router import models
|
|
||||||
|
|
||||||
DEFAULT_ENABLED_SERVICES = ['router']
|
|
||||||
VALID_SERVICES = ['router', 'loadbalancer']
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateNotFound(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def execute(args, root_helper=None):
|
|
||||||
if root_helper:
|
|
||||||
cmd = shlex.split(root_helper) + args
|
|
||||||
else:
|
|
||||||
cmd = args
|
|
||||||
try:
|
|
||||||
return subprocess.check_output(map(str, cmd), stderr=subprocess.STDOUT)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
# The serialization layer doesn't know about the extra output
|
|
||||||
# attribute of the CalledProcessError, so we don't get
|
|
||||||
# stdout/stderr. Convert to a simpler exception type and
|
|
||||||
# include all of the info from the original exception in the
|
|
||||||
# message.
|
|
||||||
raise RuntimeError('%s: %s' % (e, e.output))
|
|
||||||
|
|
||||||
|
|
||||||
def replace_file(file_name, data):
|
|
||||||
"""Replaces the contents of file_name with data in a safe manner.
|
|
||||||
|
|
||||||
First write to a temp file and then rename. Since POSIX renames are
|
|
||||||
atomic, the file is unlikely to be corrupted by competing writes.
|
|
||||||
|
|
||||||
We create the tempfile on the same device to ensure that it can be renamed.
|
|
||||||
"""
|
|
||||||
base_dir = os.path.dirname(os.path.abspath(file_name))
|
|
||||||
tmp_file = tempfile.NamedTemporaryFile('w+', dir=base_dir, delete=False)
|
|
||||||
tmp_file.write(data)
|
|
||||||
tmp_file.close()
|
|
||||||
os.chmod(tmp_file.name, 0644)
|
|
||||||
os.rename(tmp_file.name, file_name)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_directory(dir_path):
|
|
||||||
if not os.path.isdir(dir_path):
|
|
||||||
os.makedirs(dir_path, 0755)
|
|
||||||
|
|
||||||
|
|
||||||
class ModelSerializer(json.JSONEncoder):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
def default(self, obj):
|
|
||||||
if isinstance(obj, set):
|
|
||||||
return list(obj)
|
|
||||||
elif isinstance(obj, netaddr.IPNetwork):
|
|
||||||
return str(obj)
|
|
||||||
elif isinstance(obj, netaddr.IPAddress):
|
|
||||||
return str(obj)
|
|
||||||
elif isinstance(obj, models.ModelBase):
|
|
||||||
if hasattr(obj, 'to_dict'):
|
|
||||||
return obj.to_dict()
|
|
||||||
else:
|
|
||||||
return vars(obj)
|
|
||||||
return super(ModelSerializer, self).default(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def json_response(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
retval = f(*args, **kwargs)
|
|
||||||
if isinstance(retval, flask.Response):
|
|
||||||
return retval
|
|
||||||
else:
|
|
||||||
return flask.Response(
|
|
||||||
json.dumps(retval, cls=ModelSerializer, sort_keys=True),
|
|
||||||
status=200
|
|
||||||
)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def blueprint_factory(name):
|
|
||||||
name_parts = name.split(".")[-2:]
|
|
||||||
blueprint_name = "_".join(name_parts)
|
|
||||||
url_prefix = "/" + "/".join(name_parts)
|
|
||||||
return flask.Blueprint(blueprint_name, name, url_prefix=url_prefix)
|
|
||||||
|
|
||||||
|
|
||||||
def load_template(template_file):
|
|
||||||
if not os.path.exists(template_file):
|
|
||||||
raise TemplateNotFound(
|
|
||||||
'Config template not found @ %s' % template_file)
|
|
||||||
return jinja2.Template(open(template_file).read())
|
|
||||||
|
|
||||||
|
|
||||||
def hash_file(path):
|
|
||||||
h = hashlib.md5()
|
|
||||||
with open(path, 'rb') as _in:
|
|
||||||
h.update(_in.read())
|
|
||||||
return h.hexdigest()
|
|
@ -1,18 +0,0 @@
|
|||||||
# Copyright 2011 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import pbr.version
|
|
||||||
|
|
||||||
version_info = pbr.version.VersionInfo('astara_router')
|
|
@ -1,22 +0,0 @@
|
|||||||
This directory contains elements necessary to build the Astara appliance with
|
|
||||||
the diskimage-builder from the OpenStack project.
|
|
||||||
|
|
||||||
1) Install diskimage-builder via:
|
|
||||||
|
|
||||||
pip install diskimage-builder
|
|
||||||
or source at:
|
|
||||||
http://git.openstack.org/cgit/openstack/diskimage-builder
|
|
||||||
|
|
||||||
2) Ensure a few require packages are installed:
|
|
||||||
- debootstrap
|
|
||||||
- qemu-utils
|
|
||||||
|
|
||||||
3) Add elements to path
|
|
||||||
$ export ELEMENTS_PATH=~/astara-appliance/diskimage-builder/elements
|
|
||||||
|
|
||||||
4) Build image
|
|
||||||
$ DIB_RELEASE=jessie DIB_EXTLINUX=1 disk-image-create debian vm astara
|
|
||||||
|
|
||||||
5) If you're testing with kvm, don't forget to build the nocloud iso image
|
|
||||||
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
|||||||
Install Ansible.
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
At Present there is no configuration for this element.
|
|
||||||
|
|
||||||
NOTICE
|
|
||||||
------
|
|
||||||
This element is copied from the OpenStack Tripleo project at
|
|
||||||
http://git.openstack.org/cgit/openstack/tripleo-image-elements/
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
sudo rm -fr "${TMP_MOUNT_PATH}/opt/stack/tripleo-ansible"
|
|
@ -1 +0,0 @@
|
|||||||
pip-and-virtualenv
|
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -ue
|
|
||||||
|
|
||||||
export ANSIBLE_VENV_DIR=${ANSIBLE_VENV_DIR:-"/opt/stack/venvs/ansible"}
|
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
install-packages ansible
|
|
@ -1,35 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
set -eux
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
install-packages build-essential libssl-dev libyaml-dev python-dev libxml2-dev libxslt-dev libffi-dev
|
|
||||||
|
|
||||||
virtualenv $ANSIBLE_VENV_DIR
|
|
||||||
|
|
||||||
set +u
|
|
||||||
source $ANSIBLE_VENV_DIR/bin/activate
|
|
||||||
set -u
|
|
||||||
|
|
||||||
$ANSIBLE_VENV_DIR/bin/pip install paramiko PyYAML jinja2 httplib2
|
|
||||||
|
|
||||||
$ANSIBLE_VENV_DIR/bin/pip install ansible==1.8.1
|
|
||||||
|
|
||||||
ln -s $ANSIBLE_VENV_DIR/bin/ansible /usr/local/bin/ansible
|
|
||||||
ln -s $ANSIBLE_VENV_DIR/bin/ansible-playbook /usr/local/bin/ansible-playbook
|
|
@ -1,9 +0,0 @@
|
|||||||
This is the base element for building an Astara appliance image.
|
|
||||||
|
|
||||||
Ansible is required on the local system.
|
|
||||||
|
|
||||||
Advanced service drivers may be enabled in the appliance by setting
|
|
||||||
``DIB_ASTARA_ADVANCED_SERVICES``. This defaults to enabling only the
|
|
||||||
router driver, but you may enabled other avialable drivers ie:
|
|
||||||
|
|
||||||
DIB_ASTARA_ADVANCED_SERVICES=router,loadbalancer
|
|
@ -1,3 +0,0 @@
|
|||||||
cloud-init-datasources
|
|
||||||
source-repositories
|
|
||||||
ansible
|
|
@ -1 +0,0 @@
|
|||||||
export DIB_CLOUD_INIT_DATASOURCES="ConfigDrive, NoCloud"
|
|
@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -eux
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
DIB_ASTARA_ADVANCED_SERVICES=${DIB_ASTARA_ADVANCED_SERVICES:-"router"}
|
|
||||||
|
|
||||||
APP_SRC_DIR="/tmp/astara-appliance"
|
|
||||||
|
|
||||||
[ -d "${APP_SRC_DIR}" ] || exit 0
|
|
||||||
|
|
||||||
ansible-playbook -i "localhost," -c local -e enabled_advanced_services="$DIB_ASTARA_ADVANCED_SERVICES" $APP_SRC_DIR/ansible/main.yml
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ensure the locale is properly setup
|
|
||||||
sed -i 's/^# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
|
|
||||||
locale-gen
|
|
@ -1 +0,0 @@
|
|||||||
astara git /tmp/astara-appliance https://github.com/openstack/astara-appliance.git
|
|
@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
Creates a sudo privileged user in the appliance VM that can be used for
|
|
||||||
debugging connectivity issues via the console, when SSH connectivity is
|
|
||||||
not possible. Note that an 'astara' user is created by the RUG and setup
|
|
||||||
to authenticate using a SSH public key. This element should only be included
|
|
||||||
when building images for develoment environments.
|
|
||||||
|
|
||||||
The username and password can be set in the build environment as
|
|
||||||
$DIB_ASTARA_APPLIANCE_DEBUG_USER and $DIB_ASTARA_APPLIANCE_DEBUG_PASSWORD
|
|
||||||
The defaults are astara-debug/astara.
|
|
@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
DIB_ASTARA_APPLIANCE_DEBUG_USER=${DIB_ASTARA_APPLIANCE_DEBUG_USER:-astara-debug}
|
|
||||||
DIB_ASTARA_APPLIANCE_DEBUG_PASSWORD=${DIB_ASTARA_APPLIANCE_DEBUG_PASSWORD:-astara}
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
useradd -m $DIB_ASTARA_APPLIANCE_DEBUG_USER -s /bin/bash
|
|
||||||
|
|
||||||
passwd $DIB_ASTARA_APPLIANCE_DEBUG_USER <<EOF
|
|
||||||
$DIB_ASTARA_APPLIANCE_DEBUG_PASSWORD
|
|
||||||
$DIB_ASTARA_APPLIANCE_DEBUG_PASSWORD
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > /etc/sudoers.d/astara-debug-user <<eof
|
|
||||||
$DIB_ASTARA_APPLIANCE_DEBUG_USER ALL=(ALL) NOPASSWD:ALL
|
|
||||||
eof
|
|
||||||
|
|
||||||
chmod 0440 /etc/sudoers.d/astara-debug-user
|
|
||||||
visudo -c
|
|
@ -1 +0,0 @@
|
|||||||
vim:
|
|
@ -1,10 +0,0 @@
|
|||||||
This element requires that you have already downloaded a crt and key file from
|
|
||||||
nginx.
|
|
||||||
|
|
||||||
Download nginx-repo.crt and nginx-repo.key into any directory. Then when
|
|
||||||
executing set DIB_NGINX_PLUS_CERT_PATH to that directory.
|
|
||||||
|
|
||||||
i.e.
|
|
||||||
|
|
||||||
ELEMENTS_PATH=diskimage-builder/elements DIB_RELEASE=wheezy DIB_EXTLINUX=1 DIB_NGINX_PLUS_CERT_PATH=/home/david/nginxcerts \
|
|
||||||
disk-image-create debian vm nginx-plus -o nginxplus
|
|
@ -1 +0,0 @@
|
|||||||
source-repositories
|
|
@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
# Copyright (c) 2015 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
if [ -z "${DIB_NGINX_PLUS_CERT_PATH}" ]; then
|
|
||||||
echo "DIB_NGINX_PLUS_CERT_PATH is not set - not importing keys"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat $DIB_NGINX_PLUS_CERT_PATH/nginx-repo.crt >> $TMP_HOOKS_PATH/nginx-repo.crt
|
|
||||||
cat $DIB_NGINX_PLUS_CERT_PATH/nginx-repo.key >> $TMP_HOOKS_PATH/nginx-repo.key
|
|
@ -1 +0,0 @@
|
|||||||
nginx-plus
|
|
@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash -xe
|
|
||||||
# Copyright (c) 2015 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
rm -rf /etc/nginx/conf.d/default
|
|
@ -1,34 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
# Copyright (c) 2015 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
# This scripts preps apt with the nginx plus repo
|
|
||||||
|
|
||||||
# install required deps for this script
|
|
||||||
apt-get install lsb-release ca-certificates krb5-locales libsasl2-modules apt-utils apt-transport-https wget -y
|
|
||||||
|
|
||||||
# make nginx ssl dir
|
|
||||||
mkdir -p /etc/ssl/nginx
|
|
||||||
cp /tmp/in_target.d/nginx-repo.crt /etc/ssl/nginx/
|
|
||||||
cp /tmp/in_target.d/nginx-repo.key /etc/ssl/nginx/
|
|
||||||
|
|
||||||
wget https://cs.nginx.com/static/files/CA.crt -P /etc/ssl/nginx/
|
|
||||||
wget http://nginx.org/keys/nginx_signing.key -P /etc/ssl/nginx/
|
|
||||||
|
|
||||||
apt-key add /etc/ssl/nginx/nginx_signing.key
|
|
||||||
|
|
||||||
printf "deb https://plus-pkgs.nginx.com/debian `lsb_release -cs` nginx-plus\n" >/etc/apt/sources.list.d/nginx-plus.list
|
|
||||||
|
|
||||||
wget https://cs.nginx.com/static/files/90nginx -P /etc/apt/apt.conf.d
|
|
||||||
|
|
||||||
apt-get update
|
|
@ -1,2 +0,0 @@
|
|||||||
This element installs the open source nginx package.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
|||||||
package-installs
|
|
||||||
astara
|
|
@ -1 +0,0 @@
|
|||||||
nginx
|
|
@ -1,19 +0,0 @@
|
|||||||
#!/bin/bash -xe
|
|
||||||
# Copyright (c) 2015 Akanda, Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
rm -rf /etc/nginx/sites-enabled/default
|
|
||||||
|
|
||||||
# astara API appliance runs as gunicorn user and needs to create files here
|
|
||||||
chown gunicorn.gunicorn /etc/nginx/sites-enabled
|
|
@ -1,27 +0,0 @@
|
|||||||
# Configuration for astara-rootwrap
|
|
||||||
# This file should be owned by (and only-writeable by) the root user
|
|
||||||
|
|
||||||
[DEFAULT]
|
|
||||||
# List of directories to load filter definitions from (separated by ',').
|
|
||||||
# These directories MUST all be only writeable by root !
|
|
||||||
filters_path=/etc/rootwrap.d
|
|
||||||
|
|
||||||
# List of directories to search executables in, in case filters do not
|
|
||||||
# explicitely specify a full path (separated by ',')
|
|
||||||
# If not specified, defaults to system PATH environment variable.
|
|
||||||
# These directories MUST all be only writeable by root !
|
|
||||||
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/bin
|
|
||||||
|
|
||||||
# Enable logging to syslog
|
|
||||||
# Default value is False
|
|
||||||
use_syslog=False
|
|
||||||
|
|
||||||
# Which syslog facility to use.
|
|
||||||
# Valid values include auth, authpriv, syslog, local0, local1...
|
|
||||||
# Default value is 'syslog'
|
|
||||||
syslog_log_facility=syslog
|
|
||||||
|
|
||||||
# Which messages to log.
|
|
||||||
# INFO means log all usage
|
|
||||||
# ERROR means only log unsuccessful attempts
|
|
||||||
syslog_log_level=ERROR
|
|
@ -1,48 +0,0 @@
|
|||||||
# astara-rootwrap command filters for astara-appliance
|
|
||||||
# This file should be owned by (and only-writeable by) the root user
|
|
||||||
|
|
||||||
[Filters]
|
|
||||||
# astara_router/drivers/bird.py:
|
|
||||||
mv_bird: RegExpFilter, mv, root, mv, /tmp/bird6\.conf, /etc/bird/bird6\.conf
|
|
||||||
|
|
||||||
# astara_router/drivers/arp.py: 'arp'..
|
|
||||||
arp: CommandFilter, /usr/sbin/arp, root
|
|
||||||
astara_gratuitous_arp: CommandFilter, astara-gratuitous-arp, root
|
|
||||||
|
|
||||||
# astara_router/drivers/conntrackd.py:
|
|
||||||
mv_conntrackd: RegExpFilter, mv, root, mv, /tmp/conntrackd\.conf, /etc/conntrackd/conntrackd\.conf
|
|
||||||
|
|
||||||
# astara_router/drivers/dnsmasq.py:
|
|
||||||
mv_dnsmasq: RegExpFilter, mv, root, mv, /tmp/dnsmasq\.conf, /etc/dnsmasq\.d/.*\.conf
|
|
||||||
rm: CommandFilter, rm, root
|
|
||||||
|
|
||||||
# astara_router/drivers/hostname.py:
|
|
||||||
hostname: CommandFilter, /bin/hostname, root
|
|
||||||
mv_hostname: RegExpFilter, mv, root, mv, /tmp/hostname, /etc/hostname
|
|
||||||
mv_hosts: RegExpFilter, mv, root, mv, /tmp/hosts, /etc/hosts
|
|
||||||
|
|
||||||
# astara_router/drivers/ip.py:
|
|
||||||
ip: IpFilter, ip, root
|
|
||||||
sysctl: CommandFilter, sysctl, root
|
|
||||||
conntrack: CommandFilter, conntrack, root
|
|
||||||
|
|
||||||
# astara_router/drivers/keepalived.py:
|
|
||||||
mv_keepalived: RegExpFilter, mv, root, mv, /tmp/keepalived\.conf, /etc/keepalived/keepalived\.conf
|
|
||||||
|
|
||||||
# astara_router/drivers/ping.py:
|
|
||||||
ping: CommandFilter, ping, root
|
|
||||||
ping6: CommandFilter, ping6, root
|
|
||||||
|
|
||||||
# astara_router/drivers/iptables.py:
|
|
||||||
mv_rules: RegExpFilter, mv, root, mv, /tmp/ip.*tables\.rules, /etc/iptables/rules\.v.*
|
|
||||||
iptables: CommandFilter, iptables, root
|
|
||||||
ip6tables: CommandFilter, ip6tables, root
|
|
||||||
|
|
||||||
# astara_router/drivers/metadata.py:
|
|
||||||
mv_metadata: RegExpFilter, mv, root, mv, /tmp/metadata\.conf, /etc/metadata\.conf
|
|
||||||
|
|
||||||
# astara_router/drivers/vpn/ipsec.py:
|
|
||||||
mv_strongswan: RegExpFilter, mv, root, mv, /tmp/ipsec.*, /etc/ipsec.*
|
|
||||||
|
|
||||||
# astara services
|
|
||||||
services: CommandFilter, service, root
|
|
@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
features:
|
|
||||||
- The appliance is now built with conntrackd installed and supports configuring
|
|
||||||
the connection tracking service among pairs of clustered HA router appliances.
|
|
@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
fixes:
|
|
||||||
- Bug `1531651 <https://bugs.launchpad.net/astara/+bug/1531651/>`_ \- Fixes a stale interface cache from preventing additional router interfaces from being attached
|
|
@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
prelude: Astara Appliance Mitaka Series Release v8.0.0.
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
features:
|
|
||||||
- The appliance is now built with keepalived installed and supports receiving
|
|
||||||
cluster configuration from ``astara-orchestartor``, which allows pairs of
|
|
||||||
appliance VMs to cluster among themselves, providing HA routers.
|
|
@ -1,275 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
# Astara Release Notes documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Tue Nov 3 17:40:50 2015.
|
|
||||||
#
|
|
||||||
# This file is execfile()d with the current directory set to its
|
|
||||||
# containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
|
||||||
# needs_sphinx = '1.0'
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = [
|
|
||||||
'oslosphinx',
|
|
||||||
'reno.sphinxext',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The encoding of source files.
|
|
||||||
# source_encoding = 'utf-8-sig'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'Astara Appliance Release Notes'
|
|
||||||
copyright = u'2015, Astara Developers'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
from astara_router.version import version_info as astara_version
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = astara_version.version_string_with_vcs()
|
|
||||||
# The short X.Y version.
|
|
||||||
version = astara_version.canonical_version_string()
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
# language = None
|
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
|
||||||
# non-false value, then it is used:
|
|
||||||
# today = ''
|
|
||||||
# Else, today_fmt is used as the format for a strftime call.
|
|
||||||
# today_fmt = '%B %d, %Y'
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
exclude_patterns = []
|
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all
|
|
||||||
# documents.
|
|
||||||
# default_role = None
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
# add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
# add_module_names = True
|
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
|
||||||
# output. They are ignored by default.
|
|
||||||
# show_authors = False
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
|
||||||
# modindex_common_prefix = []
|
|
||||||
|
|
||||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
|
||||||
# keep_warnings = False
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
html_theme = 'default'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
# html_theme_options = {}
|
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
|
||||||
# html_theme_path = []
|
|
||||||
|
|
||||||
# The name for this set of Sphinx documents. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
# html_title = None
|
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
||||||
# html_short_title = None
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
|
||||||
# of the sidebar.
|
|
||||||
# html_logo = None
|
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
|
||||||
# pixels large.
|
|
||||||
# html_favicon = None
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
# Add any extra paths that contain custom files (such as robots.txt or
|
|
||||||
# .htaccess) here, relative to this directory. These files are copied
|
|
||||||
# directly to the root of the documentation.
|
|
||||||
# html_extra_path = []
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
# html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
|
||||||
# typographically correct entities.
|
|
||||||
# html_use_smartypants = True
|
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
|
||||||
# html_sidebars = {}
|
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
|
||||||
# template names.
|
|
||||||
# html_additional_pages = {}
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
# html_domain_indices = True
|
|
||||||
|
|
||||||
# If false, no index is generated.
|
|
||||||
# html_use_index = True
|
|
||||||
|
|
||||||
# If true, the index is split into individual pages for each letter.
|
|
||||||
# html_split_index = False
|
|
||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
|
||||||
# html_show_sourcelink = True
|
|
||||||
|
|
||||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
|
||||||
# html_show_sphinx = True
|
|
||||||
|
|
||||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
|
||||||
# html_show_copyright = True
|
|
||||||
|
|
||||||
# If true, an OpenSearch description file will be output, and all pages will
|
|
||||||
# contain a <link> tag referring to it. The value of this option must be the
|
|
||||||
# base URL from which the finished HTML is served.
|
|
||||||
# html_use_opensearch = ''
|
|
||||||
|
|
||||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
|
||||||
# html_file_suffix = None
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'AstaraReleaseNotesdoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
|
||||||
|
|
||||||
latex_elements = {
|
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
|
||||||
# 'papersize': 'letterpaper',
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
# 'pointsize': '10pt',
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
# 'preamble': '',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title,
|
|
||||||
# author, documentclass [howto, manual, or own class]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'AstaraReleaseNotes.tex', u'Astara Release Notes Documentation',
|
|
||||||
u'Astara Developers', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
|
||||||
# the title page.
|
|
||||||
# latex_logo = None
|
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
|
||||||
# not chapters.
|
|
||||||
# latex_use_parts = False
|
|
||||||
|
|
||||||
# If true, show page references after internal links.
|
|
||||||
# latex_show_pagerefs = False
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
# latex_show_urls = False
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
# latex_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
# latex_domain_indices = True
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
|
||||||
# (source start file, name, description, authors, manual section).
|
|
||||||
man_pages = [
|
|
||||||
('index', 'astarareleasenotes',
|
|
||||||
u'Astara Appliance Release Notes Documentation',
|
|
||||||
[u'Astara Developers'], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
# man_show_urls = False
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
|
||||||
|
|
||||||
# Grouping the document tree into Texinfo files. List of tuples
|
|
||||||
# (source start file, target name, title, author,
|
|
||||||
# dir menu entry, description, category)
|
|
||||||
texinfo_documents = [
|
|
||||||
('index', 'AstaraReleaseNotes',
|
|
||||||
u'Astara Appliance Release Notes Documentation',
|
|
||||||
u'Astara Developers', 'AstaraApplianceReleaseNotes',
|
|
||||||
'One line description of project.',
|
|
||||||
'Miscellaneous'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
# texinfo_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
# texinfo_domain_indices = True
|
|
||||||
|
|
||||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
|
||||||
# texinfo_show_urls = 'footnote'
|
|
||||||
|
|
||||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
|
||||||
# texinfo_no_detailmenu = False
|
|
@ -1,8 +0,0 @@
|
|||||||
==============================
|
|
||||||
Astara Appliance Release Notes
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
mitaka
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user