Compare commits
	
		
			37 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					300d583be5 | ||
| 
						 | 
					74a59f57e4 | ||
| 
						 | 
					cddbee877b | ||
| 
						 | 
					14443cb675 | ||
| 
						 | 
					5152121d2b | ||
| 
						 | 
					026e07c51b | ||
| 
						 | 
					2bd6fa418b | ||
| 
						 | 
					74b66c959f | ||
| 
						 | 
					8b2a34d5d7 | ||
| 
						 | 
					3b11b46a39 | ||
| 
						 | 
					1dcb08352a | ||
| 
						 | 
					e96c505367 | ||
| 
						 | 
					ec03d095d9 | ||
| 
						 | 
					c8086b0901 | ||
| 
						 | 
					83d2b7bc9b | ||
| 58ea1d8d79 | |||
| b3d4729432 | |||
| c660fbeec5 | |||
| e86f96d16d | |||
| c7ab0bcb5d | |||
| 19f3be3c91 | |||
| 966f5d48ab | |||
| e590ea5aaa | |||
| 6f4cff383f | |||
| 47a725ed4d | |||
| 720c96f494 | |||
| e00775cd67 | |||
| f7bbc52cfb | |||
| d26f7169ec | |||
| ec7d5c5fcc | |||
| 0ff18ea0de | |||
| ce5dbc6886 | |||
| 2487c4141a | |||
| 71125cd072 | |||
| 43dab05942 | |||
| 0bcbf12212 | |||
| c1fcebb5e5 | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					.idea/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.python-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.python-version
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					3.14.0
 | 
				
			||||||
							
								
								
									
										13
									
								
								Pipfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Pipfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					[[source]]
 | 
				
			||||||
 | 
					url = "https://pypi.org/simple"
 | 
				
			||||||
 | 
					verify_ssl = true
 | 
				
			||||||
 | 
					name = "pypi"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[packages]
 | 
				
			||||||
 | 
					pyaml = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dev-packages]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[requires]
 | 
				
			||||||
 | 
					python_version = "3.14"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										109
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "_meta": {
 | 
				
			||||||
 | 
					        "hash": {
 | 
				
			||||||
 | 
					            "sha256": "635a55e0b494b99462b0df92263ceefd638deacc42eb60bfa9c8a8ff02ed66a3"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "pipfile-spec": 6,
 | 
				
			||||||
 | 
					        "requires": {
 | 
				
			||||||
 | 
					            "python_version": "3.14"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "sources": [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "name": "pypi",
 | 
				
			||||||
 | 
					                "url": "https://pypi.org/simple",
 | 
				
			||||||
 | 
					                "verify_ssl": true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "default": {
 | 
				
			||||||
 | 
					        "pyaml": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:ce5d7867cc2b455efdb9b0448324ff7b9f74d99f64650f12ca570102db6b985f",
 | 
				
			||||||
 | 
					                "sha256:e113a64ec16881bf2b092e2beb84b7dcf1bd98096ad17f5f14e8fb782a75d99b"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "index": "pypi",
 | 
				
			||||||
 | 
					            "markers": "python_version >= '3.8'",
 | 
				
			||||||
 | 
					            "version": "==25.7.0"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "pyyaml": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c",
 | 
				
			||||||
 | 
					                "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a",
 | 
				
			||||||
 | 
					                "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3",
 | 
				
			||||||
 | 
					                "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956",
 | 
				
			||||||
 | 
					                "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6",
 | 
				
			||||||
 | 
					                "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c",
 | 
				
			||||||
 | 
					                "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65",
 | 
				
			||||||
 | 
					                "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a",
 | 
				
			||||||
 | 
					                "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0",
 | 
				
			||||||
 | 
					                "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b",
 | 
				
			||||||
 | 
					                "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1",
 | 
				
			||||||
 | 
					                "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6",
 | 
				
			||||||
 | 
					                "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7",
 | 
				
			||||||
 | 
					                "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e",
 | 
				
			||||||
 | 
					                "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007",
 | 
				
			||||||
 | 
					                "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310",
 | 
				
			||||||
 | 
					                "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4",
 | 
				
			||||||
 | 
					                "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9",
 | 
				
			||||||
 | 
					                "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295",
 | 
				
			||||||
 | 
					                "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea",
 | 
				
			||||||
 | 
					                "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0",
 | 
				
			||||||
 | 
					                "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e",
 | 
				
			||||||
 | 
					                "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac",
 | 
				
			||||||
 | 
					                "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9",
 | 
				
			||||||
 | 
					                "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7",
 | 
				
			||||||
 | 
					                "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35",
 | 
				
			||||||
 | 
					                "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb",
 | 
				
			||||||
 | 
					                "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b",
 | 
				
			||||||
 | 
					                "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69",
 | 
				
			||||||
 | 
					                "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5",
 | 
				
			||||||
 | 
					                "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b",
 | 
				
			||||||
 | 
					                "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c",
 | 
				
			||||||
 | 
					                "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369",
 | 
				
			||||||
 | 
					                "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd",
 | 
				
			||||||
 | 
					                "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824",
 | 
				
			||||||
 | 
					                "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198",
 | 
				
			||||||
 | 
					                "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065",
 | 
				
			||||||
 | 
					                "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c",
 | 
				
			||||||
 | 
					                "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c",
 | 
				
			||||||
 | 
					                "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764",
 | 
				
			||||||
 | 
					                "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196",
 | 
				
			||||||
 | 
					                "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b",
 | 
				
			||||||
 | 
					                "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00",
 | 
				
			||||||
 | 
					                "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac",
 | 
				
			||||||
 | 
					                "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8",
 | 
				
			||||||
 | 
					                "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e",
 | 
				
			||||||
 | 
					                "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28",
 | 
				
			||||||
 | 
					                "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3",
 | 
				
			||||||
 | 
					                "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5",
 | 
				
			||||||
 | 
					                "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4",
 | 
				
			||||||
 | 
					                "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b",
 | 
				
			||||||
 | 
					                "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf",
 | 
				
			||||||
 | 
					                "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5",
 | 
				
			||||||
 | 
					                "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702",
 | 
				
			||||||
 | 
					                "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8",
 | 
				
			||||||
 | 
					                "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788",
 | 
				
			||||||
 | 
					                "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da",
 | 
				
			||||||
 | 
					                "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d",
 | 
				
			||||||
 | 
					                "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc",
 | 
				
			||||||
 | 
					                "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c",
 | 
				
			||||||
 | 
					                "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba",
 | 
				
			||||||
 | 
					                "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f",
 | 
				
			||||||
 | 
					                "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917",
 | 
				
			||||||
 | 
					                "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5",
 | 
				
			||||||
 | 
					                "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26",
 | 
				
			||||||
 | 
					                "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f",
 | 
				
			||||||
 | 
					                "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b",
 | 
				
			||||||
 | 
					                "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be",
 | 
				
			||||||
 | 
					                "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c",
 | 
				
			||||||
 | 
					                "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3",
 | 
				
			||||||
 | 
					                "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6",
 | 
				
			||||||
 | 
					                "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926",
 | 
				
			||||||
 | 
					                "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "markers": "python_version >= '3.8'",
 | 
				
			||||||
 | 
					            "version": "==6.0.3"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "develop": {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										158
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										158
									
								
								README.md
									
									
									
									
									
								
							@@ -7,47 +7,91 @@ The Linux SSH client has many configuration options to choose from, and sometime
 | 
				
			|||||||
Written by Mike Peralta and released to the public via GPLv3 on Sunday, May 27th, 2018
 | 
					Written by Mike Peralta and released to the public via GPLv3 on Sunday, May 27th, 2018
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Requirements
 | 
					## Requirements
 | 
				
			||||||
* Linux (possibly others)
 | 
					 | 
				
			||||||
* Python 3
 | 
					 | 
				
			||||||
* NetworkManager and nmcli (tested on v1.10.6)
 | 
					 | 
				
			||||||
* One of the following: root access, the ability to place executable files inside NetworkManager's dispatcher directory, or the ability to call this script whenever connection details change
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note: This script has only been tested with NetworkManager and nmcli 1.10.6, but should work with any other daemon or application that can call this script with the required parameters.
 | 
					  * Linux (possibly others)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * Python 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * pyenv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * pipenv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * NetworkManager and nmcli (tested on v1.46)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * One of the following: root access, the ability to place executable files inside NetworkManager's dispatcher directory, or the ability to call this script whenever connection details change
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note: This script has only been tested with NetworkManager and nmcli 1.46, but should work with any other daemon or application that can call this script with the required parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You can also try using this script with a different daemon (or your own custom script). All you need to do is call this script whenever some change in the network occurs, with the following parameters:
 | 
					You can also try using this script with a different daemon (or your own custom script). All you need to do is call this script whenever some change in the network occurs, with the following parameters:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```connection-specific-ssh-config <interface> <command> <ini_path>```
 | 
					```bash
 | 
				
			||||||
 | 
					$ cd path/to/connection-specific-ssh-config && pipenv run python main.py --config <config_path> --adapter <adapter> --command <command>
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * Where *config_path* is the path to a yaml configuration file, explained below
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * Where *adapter* is the name of the adapter/interface that has changed states
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * Where *command* is the type of change occurring. Currently, the script only does anything when receiving the `up` command.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Where *interface* is the name of whatever interface just changed
 | 
					 | 
				
			||||||
* Where *command* is the type of change occurring. Currently, the script only does anything when receiving the "up" command.
 | 
					 | 
				
			||||||
* Where *ini_path* is the path to an ini configuration file, explained below
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Installation (NetworkManager)
 | 
					## Installation (NetworkManager)
 | 
				
			||||||
1. Copy this script somewhere safe, such as ```/path/to/connection-specific-ssh-config```
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. Move your old ssh configuration file (typically at *~/.ssh/config*) to a safe backup, like:
 | 
					This section to help with installation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   ```mv ~/.ssh/config config-backup```
 | 
					### Python Environment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
3. Create as many customized ssh configuration files as you need
 | 
					  1. Install [pyenv](https://github.com/pyenv/pyenv)
 | 
				
			||||||
 | 
					  2. Install [pipenv](https://pipenv.pypa.io/en/latest/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
4. Create a configuration file somewhere safe, such as ```/path/to/connection-specific-ssh-config.ini``` (explained in detail further below)
 | 
					Setting up the *pipenv* environment is pretty straight forward:
 | 
				
			||||||
   
 | 
					 | 
				
			||||||
5. Open a terminal and navigate to NetworkManager's dispatcher directory, often found here:
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
   ```cd /etc/NetworkManager/dispatcher.d```
 | 
					```bash
 | 
				
			||||||
 | 
					$ cd /path/to/connection-specific-ssh-config
 | 
				
			||||||
 | 
					$ pyenv update
 | 
				
			||||||
 | 
					$ pyenv install
 | 
				
			||||||
 | 
					$ pip install --upgrade pip pipenv
 | 
				
			||||||
 | 
					$ pipenv sync
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
6. Create a short bash script inside the dispatcher directory that will launch connection-specific-ssh-config. For instance, you might name this file ```/etc/NetworkManager/dispatcher.d/99-launch-connection-specific-ssh-config```
 | 
					You should then be able to get a help menu with:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
7. Inside your launcher script, put the following contents:
 | 
					```bash
 | 
				
			||||||
  ```
 | 
					$ cd /path/to/connection-specific-ssh-config
 | 
				
			||||||
  #!/bin/bash
 | 
					$ pipenv run python main.py --help
 | 
				
			||||||
  
 | 
					```
 | 
				
			||||||
  /path/to/connection-specific-ssh-config "$1" "$2" "/path/to/connection-specific-ssh-config.ini"
 | 
					 | 
				
			||||||
  ```
 | 
					 | 
				
			||||||
  This will take the two important variables which NetworkManager has passed along to your launcher script, and pass them along to connection-specific-ssh-config
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
8. Repeat steps  2-7 for each additional user who would like connection based ssh configuration files.
 | 
					### SSH Config Preparation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Move your old ssh configuration file (typically at *~/.ssh/config*) to a safe backup, like:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * ```mv ~/.ssh/config config-backup```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Create as many customized ssh configuration files as you need, such as `config-remote` or `config-home`. Do not name any of them `config`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Configuration File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Create a configuration file somewhere safe, such as ```/path/to/connection-specific-ssh-config.yaml``` (explained in detail further below)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Network Manager Dispatch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Open a terminal and navigate to NetworkManager's dispatcher directory, often found here:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * ```cd /etc/NetworkManager/dispatcher.d```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * There may be subdirectories underneath. Just ignore them.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Create a short bash script inside the dispatcher directory that will launch *connection-specific-ssh-config*. For instance, you might name this file ```/etc/NetworkManager/dispatcher.d/99-launch-connection-specific-ssh-config```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. Inside your launcher script, put the following contents:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cd /path/to/connection-specific-ssh-config && pipenv run python main.py --config /path/to/your/config.yaml --interface "$1" --command "$2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * This will take the two important variables which NetworkManager has passed along to your launcher script, and give them to *connection-specific-ssh-config*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. Repeat steps  1-3 for each additional user who would like connection based ssh configuration files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Example Scenarios
 | 
					## Example Scenarios
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,43 +106,47 @@ Why would you use this script? Suppose the following scenarios:
 | 
				
			|||||||
Each of these scenarios can be solved by the connection-specific-ssh-config script. Simply create as many ssh configuration files as you need, and each can be dynamically symlinked as you connec to different networks.
 | 
					Each of these scenarios can be solved by the connection-specific-ssh-config script. Simply create as many ssh configuration files as you need, and each can be dynamically symlinked as you connec to different networks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Configuration File
 | 
					## Configuration File
 | 
				
			||||||
Configuration files for connection-specific-ssh-config are just standard python/php style ini files, and are relatively simple:
 | 
					Configuration files for connection-specific-ssh-config are written in YAML and are relatively simple:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* You need a section called "config", which currently only holds one variable:
 | 
					* You need a section called `options`, which holds two variables:
 | 
				
			||||||
   * *ssh_dir*, which specifies the directory your ssh client looks for configuration files (typically *~/.ssh/*)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
* You then need one or more sections which specify targets that associate ssh configuration files with network connection names
 | 
					    1. `ssh-dir`, which specifies your ssh configuration directory (typically *~/.ssh/*)
 | 
				
			||||||
   * The key *adapter* can specify one adapter to watch
 | 
					    2. `default-target`, which specifies the default target to use if the currently connected wifi access point does not match anything configured.
 | 
				
			||||||
   * The key *adapters* can specify multiple adapters (must be written in JSON format)
 | 
					 | 
				
			||||||
   * The key *ssid* can specify one connection name
 | 
					 | 
				
			||||||
   * The key *ssids* can specify multiple connection names (must be written in JSON format)
 | 
					 | 
				
			||||||
   * The key *ssh_config_name* specifies the name of the ssh configuration file that should apply if this target activates
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
* You can then optionally add a "default" section, which specifies which default ssh configuration file to fallback to, if any
 | 
					* You then need one or more sections which specify targets that associate ssh configuration files with network connection names:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * The key `adapter` can specify one adapter to watch
 | 
				
			||||||
 | 
					    * The key `adapters` can specify multiple adapters, as a list
 | 
				
			||||||
 | 
					    * The key `ssid` can specify one connection name
 | 
				
			||||||
 | 
					    * The key `ssids` can specify multiple connection names, as a list
 | 
				
			||||||
 | 
					    * The key *config-file-name* specifies the name of the ssh configuration file that should apply if this target activates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Here's an example configuration file to help you get started:
 | 
					Here's an example configuration file to help you get started:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```yaml
 | 
				
			||||||
 | 
					options:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  default-target: remote
 | 
				
			||||||
 | 
					  ssh-dir: /home/your-username/.ssh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					targets:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  home:
 | 
				
			||||||
 | 
					    adapters:
 | 
				
			||||||
 | 
					      - wlo1
 | 
				
			||||||
 | 
					    ssids:
 | 
				
			||||||
 | 
					      - Some Wifi AP Name
 | 
				
			||||||
 | 
					      - Some Other Wifi AP Name
 | 
				
			||||||
 | 
					    config-file-name: config-home
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  remote:
 | 
				
			||||||
 | 
					    adapters:
 | 
				
			||||||
 | 
					      - wlo1
 | 
				
			||||||
 | 
					    config-file-name: config-remote
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[config]
 | 
					The above configuration file will instruct the script to use the ssh config file */home/your-username/.ssh/config-home* while you're connected to one of the access points named in the `ssids` list of the `home` target. Otherwise, the default config file */home/your-username/.ssh/config-remote* would be used. All of this would only apply to the adapter `wlo1`, but you could specify more adapters.
 | 
				
			||||||
ssh_dir = /home/mike/.ssh
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[default]
 | 
					The script works by simply creating a symlink where your original ssh configuration file was (typically *~/.ssh/config*), pointing to the ssh configuration determined to be the one you want active. Note that this of course means the script will fail (for safety reasons), if your original ssh configuration file still exists as a normal file.
 | 
				
			||||||
adapter = wlan0
 | 
					 | 
				
			||||||
ssh_config_name = config-default
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[Wifi Work]
 | 
					 | 
				
			||||||
adapters = ["wlan0", "wlan1"]
 | 
					 | 
				
			||||||
ssids = ["Work Connection - Main Office", "Work Connection - Garys Office"]
 | 
					 | 
				
			||||||
ssh_config_name = config-work
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[Wifi Home]
 | 
					 | 
				
			||||||
adapter = wlan0
 | 
					 | 
				
			||||||
ssid = My Home Connection (Oh Joy)
 | 
					 | 
				
			||||||
ssh_config_name = config-home
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The above configuration file will instruct the script to use the ssh configuration file */home/mike/.ssh/config-work* while you're connected to one of your work connections *Work Connection - Main Office* or *Work Connection - Garys Office*, on either of your wireless adapters *wlan0* or *wlan1*. When you get home and connect to your *My Home Connection (Oh Joy)* connection on *wlan0*, the config file */home/mike/.ssh/config-home* will be used instead. Finally, the configuration file */home/mike/.ssh/config-default* will be used when *wlan0* connects to some undefined network.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,229 +0,0 @@
 | 
				
			|||||||
#!/usr/bin/env python3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
import configparser
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import logging
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
import re
 | 
					 | 
				
			||||||
import subprocess
 | 
					 | 
				
			||||||
import sys
 | 
					 | 
				
			||||||
import syslog
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
class SSHConfiger:
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	_action_interface = None
 | 
					 | 
				
			||||||
	_action_command = None
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	_config_file_path = None
 | 
					 | 
				
			||||||
	_targets = None
 | 
					 | 
				
			||||||
	_config = None
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def __init__(self, action_interface, action_command, config_file_path):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Grab args
 | 
					 | 
				
			||||||
		self._action_interface = action_interface
 | 
					 | 
				
			||||||
		self._action_command = action_command
 | 
					 | 
				
			||||||
		self._config_file_path = config_file_path
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def log(self, s):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		print ("[SSHConfiger]", s)
 | 
					 | 
				
			||||||
		syslog.syslog("[SSHConfiger] " + s)
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def complain(self, s):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		syslog.syslog(syslog.LOG_ERR, "[SSHConfiger] " + s)
 | 
					 | 
				
			||||||
		print("[SSHConfiger]", s, file=sys.stderr)
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def die(self, s):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		self.complain(s)
 | 
					 | 
				
			||||||
		raise Exception(s)
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def run(self):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		self.log("Running for interface \""+ self._action_interface +"\": " + self._action_command)
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Parse the config
 | 
					 | 
				
			||||||
		self.parse_config()
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Grab the specified ssh directory
 | 
					 | 
				
			||||||
		if ( "ssh_dir" not in self._config.keys() ):
 | 
					 | 
				
			||||||
			self.die("Config file needs key \"ssh_dir\" inside \"config\" section")
 | 
					 | 
				
			||||||
		ssh_dir = self._config["ssh_dir"]
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Determine which ssh config file we should use
 | 
					 | 
				
			||||||
		ssh_config_name = self.determine_ssh_config_name()
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Make paths
 | 
					 | 
				
			||||||
		ssh_config_path_link = os.path.join(ssh_dir, "config")
 | 
					 | 
				
			||||||
		ssh_config_path_target = os.path.join(ssh_dir, ssh_config_name)
 | 
					 | 
				
			||||||
		self.log("Selecting source config file \"" + ssh_config_path_target + "\" for link \"" + ssh_config_path_link + "\"")
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Don't run unless current ssh config is a symlink or not a file (for safety)
 | 
					 | 
				
			||||||
		self.require_symlink_or_none(ssh_config_path_link)
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Set the symlink
 | 
					 | 
				
			||||||
		try:
 | 
					 | 
				
			||||||
			os.unlink(ssh_config_path_link)
 | 
					 | 
				
			||||||
		except:
 | 
					 | 
				
			||||||
			pass
 | 
					 | 
				
			||||||
		os.symlink(ssh_config_path_target, ssh_config_path_link)
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		self.log("Finished")
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def require_symlink_or_none(self, file_path):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		if ( not os.path.isfile(file_path) or os.path.islink(file_path) ):
 | 
					 | 
				
			||||||
			return True
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		self.die("For safety, we cannot continue if the target link exists and is a file (" + file_path + ")")
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def determine_ssh_config_name(self):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Only run if an interface is coming up
 | 
					 | 
				
			||||||
		if ( self._action_command != "up" ):
 | 
					 | 
				
			||||||
			return None
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Check each section
 | 
					 | 
				
			||||||
		found_ssh_config_name = None
 | 
					 | 
				
			||||||
		for section_name in self._targets:
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#
 | 
					 | 
				
			||||||
			section = self._targets[section_name]
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#	Must match at least one interface
 | 
					 | 
				
			||||||
			if ( self._action_interface not in section["adapters"] ):
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#
 | 
					 | 
				
			||||||
			interface_ssid = self.get_interface_ssid(self._action_interface)
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#	Must also match at least one SSID
 | 
					 | 
				
			||||||
			if ( interface_ssid not in section["ssids"] ):
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#	Found a match!
 | 
					 | 
				
			||||||
			found_ssh_config_name = section["ssh_config_name"]
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Didn't find anything? Go default ...
 | 
					 | 
				
			||||||
		if (found_ssh_config_name == None):
 | 
					 | 
				
			||||||
			if ( "default" in self._targets.keys() ):
 | 
					 | 
				
			||||||
				if ( "ssh_config_name" in self._targets["default"].keys() ):
 | 
					 | 
				
			||||||
					found_ssh_config_name = self._targets["default"]["ssh_config_name"]
 | 
					 | 
				
			||||||
					self.log("No matches found; Defaulting to:" + found_ssh_config_name)
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		return found_ssh_config_name
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def get_interface_ssid(self, interface):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		p = subprocess.Popen(["nmcli", "dev", "show", interface], stdout=subprocess.PIPE)
 | 
					 | 
				
			||||||
		(stdout, stderr) = p.communicate()
 | 
					 | 
				
			||||||
		stdout = stdout.decode()
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		r = re.compile("""^GENERAL.CONNECTION:\s+(?P<ssid>[^\s].+)$""", re.MULTILINE)
 | 
					 | 
				
			||||||
		match = r.search(stdout)
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return match.group("ssid")
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	def parse_config(self):
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		self.log("Parsing config: " + self._config_file_path)
 | 
					 | 
				
			||||||
		parser = configparser.ConfigParser()
 | 
					 | 
				
			||||||
		parser.read( self._config_file_path )
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Attempt to grab global config
 | 
					 | 
				
			||||||
		if ( "config" not in parser.sections() ):
 | 
					 | 
				
			||||||
			self.die("config section not found")
 | 
					 | 
				
			||||||
		config = parser["config"]
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#	Targets
 | 
					 | 
				
			||||||
		targets = {}
 | 
					 | 
				
			||||||
		for section in parser.sections():
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#	Skip the config section
 | 
					 | 
				
			||||||
			if ( section == "config" ):
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#
 | 
					 | 
				
			||||||
			target = {
 | 
					 | 
				
			||||||
				"adapters" : [],
 | 
					 | 
				
			||||||
				"ssids" : []
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#	Adapters
 | 
					 | 
				
			||||||
			if ( parser.has_option(section, "adapters") ):
 | 
					 | 
				
			||||||
				target["adapters"] = json.loads( parser[section]["adapters"] )
 | 
					 | 
				
			||||||
			if ( parser.has_option(section, "adapter") ):
 | 
					 | 
				
			||||||
				target["adapters"].append( parser[section]["adapter"] )
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#	SSIDs
 | 
					 | 
				
			||||||
			if ( parser.has_option(section, "ssids") ):
 | 
					 | 
				
			||||||
				target["ssids"] = json.loads( parser[section]["ssids"] )
 | 
					 | 
				
			||||||
			if ( parser.has_option(section, "ssid") ):
 | 
					 | 
				
			||||||
				target["ssids"].append( parser[section]["ssid"] )
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#	ssh_config_name
 | 
					 | 
				
			||||||
			if ( parser.has_option(section, "ssh_config_name") ):
 | 
					 | 
				
			||||||
				target["ssh_config_name"] = parser[section]["ssh_config_name"]
 | 
					 | 
				
			||||||
			else:
 | 
					 | 
				
			||||||
				raise Exception("ssh_config_name key missing from section: " + section)
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			#
 | 
					 | 
				
			||||||
			targets[section] = target
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		#
 | 
					 | 
				
			||||||
		self._config = config
 | 
					 | 
				
			||||||
		self._targets = targets
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#	Main Entry
 | 
					 | 
				
			||||||
if (__name__ == "__main__"):
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#	Script name
 | 
					 | 
				
			||||||
	script_name = sys.argv[0]
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#	Network Manager action info
 | 
					 | 
				
			||||||
	action_interface = sys.argv[1]
 | 
					 | 
				
			||||||
	action_command = sys.argv[2]
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#	Config file
 | 
					 | 
				
			||||||
	config_file_path = sys.argv[3]
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	#
 | 
					 | 
				
			||||||
	configger = SSHConfiger(action_interface, action_command, config_file_path)
 | 
					 | 
				
			||||||
	configger.run()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							
								
								
									
										20
									
								
								domain/Logger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								domain/Logger.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import syslog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Logger:
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@staticmethod
 | 
				
			||||||
 | 
						def log(s):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							print("[SSHConfiger]", s)
 | 
				
			||||||
 | 
							syslog.syslog("[SSHConfiger] " + s)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						#
 | 
				
			||||||
 | 
						@staticmethod
 | 
				
			||||||
 | 
						def complain(s):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							syslog.syslog(syslog.LOG_ERR, "[SSHConfiger] " + s)
 | 
				
			||||||
 | 
							print("[SSHConfiger]", s, file=sys.stderr)
 | 
				
			||||||
							
								
								
									
										196
									
								
								domain/SSHConfigChanger.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										196
									
								
								domain/SSHConfigChanger.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from domain.config.Config import Config
 | 
				
			||||||
 | 
					from domain.config.Config import Target
 | 
				
			||||||
 | 
					from domain.Logger import Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SSHConfigChanger:
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def __init__(
 | 
				
			||||||
 | 
								self,
 | 
				
			||||||
 | 
								action_interface, action_command,
 | 
				
			||||||
 | 
								config_file_path,
 | 
				
			||||||
 | 
								dry_run: bool = False,
 | 
				
			||||||
 | 
						):
 | 
				
			||||||
 | 
							self.__logger = Logger(
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__config = Config(
 | 
				
			||||||
 | 
								logger=self.__logger,
 | 
				
			||||||
 | 
								file_path=config_file_path
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if dry_run:
 | 
				
			||||||
 | 
								self.__logger.complain(f"Dry run enabled at runtime")
 | 
				
			||||||
 | 
								self.__config.dry_run = True
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# Grab args
 | 
				
			||||||
 | 
							self.__action_interface = action_interface
 | 
				
			||||||
 | 
							self.__action_command = action_command
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def die(self, s):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							#
 | 
				
			||||||
 | 
							self.__logger.complain(s)
 | 
				
			||||||
 | 
							raise Exception(s)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def quit(self, s):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							#
 | 
				
			||||||
 | 
							self.__logger.log("Quitting because: " + s)
 | 
				
			||||||
 | 
							sys.exit(0)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						#
 | 
				
			||||||
 | 
						def run(self):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__logger.log(
 | 
				
			||||||
 | 
								f"Running for interface {self.__action_interface}: {self.__action_command}"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# Only run if an interface is coming up
 | 
				
			||||||
 | 
							if self.__action_command != "up":
 | 
				
			||||||
 | 
								self.quit(f"We don't need to run for action command: {self.__action_command}")
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# Determine which ssh config file we should use
 | 
				
			||||||
 | 
							ssh_config_target = self.determine_ssh_config_target()
 | 
				
			||||||
 | 
							if ssh_config_target is None:
 | 
				
			||||||
 | 
								self.die("Unable to determine appropriate ssh config name; Quitting")
 | 
				
			||||||
 | 
							self.__logger.log(
 | 
				
			||||||
 | 
								f"Determined ssh config name: {ssh_config_target.name}"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# Make paths
 | 
				
			||||||
 | 
							ssh_config_path_link = self.__config.ssh_dir / self.__config.default_normal_ssh_config_file_name
 | 
				
			||||||
 | 
							ssh_config_path_target = self.__config.ssh_dir / ssh_config_target.ssh_config_file_name
 | 
				
			||||||
 | 
							self.__logger.log(
 | 
				
			||||||
 | 
								f"Selecting source config file \"{ssh_config_path_target}\""
 | 
				
			||||||
 | 
								f" for link \"{ssh_config_path_link}\""
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# Don't run unless current ssh config is a symlink or not a file (for safety)
 | 
				
			||||||
 | 
							self.require_symlink_or_none(ssh_config_path_link)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if self.__config.dry_run:
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								self.__logger.complain(
 | 
				
			||||||
 | 
									f"Dry run enabled; Won't unlink existing symlink: {ssh_config_path_link}"
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								self.__logger.complain(
 | 
				
			||||||
 | 
									f"Dry run enabled; Won't create symlink: {ssh_config_path_link} ==> {ssh_config_path_target}"
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
								# Set the symlink
 | 
				
			||||||
 | 
								if ssh_config_path_link.exists():
 | 
				
			||||||
 | 
									try:
 | 
				
			||||||
 | 
										ssh_config_path_link.unlink()
 | 
				
			||||||
 | 
									except FileNotFoundError:
 | 
				
			||||||
 | 
										pass
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								ssh_config_path_link.symlink_to(ssh_config_path_target, target_is_directory=False)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							#
 | 
				
			||||||
 | 
							self.__logger.log("Finished")
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						#
 | 
				
			||||||
 | 
						def require_symlink_or_none(self, file_path: Path):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if file_path.is_file() and file_path.exists() and not file_path.is_symlink():
 | 
				
			||||||
 | 
								self.die(
 | 
				
			||||||
 | 
									f"For safety, refusing to continue because the target ssh config file exists and is not a symlink:"
 | 
				
			||||||
 | 
									f" {file_path}"
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def determine_ssh_config_target(self) -> Target:
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							#
 | 
				
			||||||
 | 
							self.__logger.log("Attempting to determine SSH config name")
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# Start off by assuming the default target
 | 
				
			||||||
 | 
							# noinspection PyTypeChecker
 | 
				
			||||||
 | 
							selected_target = None
 | 
				
			||||||
 | 
							selected_target: Target
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# Check each section
 | 
				
			||||||
 | 
							for target_name in self.__config.targets:
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								self.__logger.log(f"Examining target: {target_name}")
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								if selected_target is not None and target_name == selected_target.name:
 | 
				
			||||||
 | 
									self.__logger.log(f"Ignoring target because it is already selected: {target_name}")
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								target = self.__config.targets[target_name]
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								# Matches, if current interface found in adapters
 | 
				
			||||||
 | 
								if self.__action_interface not in target.adapters:
 | 
				
			||||||
 | 
									self.__logger.log(
 | 
				
			||||||
 | 
										f"Target \"{target_name}\" didn't match any interfaces; Skipping"
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								# Grab the SSID this adapter is currently connected to
 | 
				
			||||||
 | 
								interface_ssid = self.get_interface_ssid(self.__action_interface)
 | 
				
			||||||
 | 
								if not interface_ssid:
 | 
				
			||||||
 | 
									self.__logger.log(
 | 
				
			||||||
 | 
										f"Interface \"{interface_ssid}\" isn't connected to anything; Done looking"
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								self.__logger.log(
 | 
				
			||||||
 | 
									f"Interface \"{self.__action_interface}\" is currently connected to: \"{interface_ssid}\""
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								# Must also match at least one SSID
 | 
				
			||||||
 | 
								if interface_ssid in target.ssids:
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									self.__logger.log(
 | 
				
			||||||
 | 
										f"Found SSID \"{interface_ssid}\" in target {target_name}"
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									# Only override selected target if this one has less SSIDs,
 | 
				
			||||||
 | 
									# or there is no currently selected target
 | 
				
			||||||
 | 
									if selected_target is None:
 | 
				
			||||||
 | 
										self.__logger.log(
 | 
				
			||||||
 | 
											f"Found first suitable target: {target_name}"
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
										selected_target = target
 | 
				
			||||||
 | 
									if len(target.ssids) < len(selected_target.ssids):
 | 
				
			||||||
 | 
										self.__logger.log(
 | 
				
			||||||
 | 
											f"Target \"{target_name}\""
 | 
				
			||||||
 | 
											f" seems to be a better match than \"{selected_target.name}\""
 | 
				
			||||||
 | 
											f" because it has fewer specified SSIDs"
 | 
				
			||||||
 | 
											f" ({len(target.ssids)} vs. {len(selected_target.ssids)})"
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
										selected_target = target
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if selected_target is None:
 | 
				
			||||||
 | 
								selected_target = self.__config.targets[self.__config.default_target_name]
 | 
				
			||||||
 | 
								self.__logger.log(f"No suitable target found; Defaulting to: {selected_target.name}")
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return selected_target
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@staticmethod
 | 
				
			||||||
 | 
						def get_interface_ssid(interface_name):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							#
 | 
				
			||||||
 | 
							p = subprocess.Popen(["nmcli", "dev", "show", interface_name], stdout=subprocess.PIPE)
 | 
				
			||||||
 | 
							(stdout, stderr) = p.communicate()
 | 
				
			||||||
 | 
							stdout = stdout.decode()
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							#
 | 
				
			||||||
 | 
							r = re.compile(
 | 
				
			||||||
 | 
								pattern=r"^GENERAL.CONNECTION:\s+(?P<ssid>[^\s].+)$",
 | 
				
			||||||
 | 
								flags=re.MULTILINE
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							match = r.search(stdout)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return match.group("ssid")
 | 
				
			||||||
							
								
								
									
										278
									
								
								domain/config/Config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								domain/config/Config.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,278 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from domain.Logger import Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					import yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Target:
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def __init__(self, logger: Logger, name):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__logger = logger
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__name = name
 | 
				
			||||||
 | 
							self.__data = {}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__adapters_names = []
 | 
				
			||||||
 | 
							self.__ssids = []
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							# noinspection PyTypeChecker
 | 
				
			||||||
 | 
							self.__ssh_config_file_name: str = None
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def __str__(self):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							s = ""
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							s += f"Target: {self.__name}"
 | 
				
			||||||
 | 
							s += f"\n> SSH config file name: {self.__ssh_config_file_name}"
 | 
				
			||||||
 | 
							s += f"\n> Adapters: "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(self.__adapters_names) > 0:
 | 
				
			||||||
 | 
								s += ", ".join(self.__adapters_names)
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								s += "[none]"
 | 
				
			||||||
 | 
							s += f"\n> SSIDs: "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(self.__ssids) > 0:
 | 
				
			||||||
 | 
								s += ", ".join(self.__ssids)
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								s += "[none]"
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return s
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def log(self, s):
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							self.__logger.log(
 | 
				
			||||||
 | 
								f"[Target::{self.__name}] {s}"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def complain(self, s):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__logger.complain(
 | 
				
			||||||
 | 
								f"[Target::{self.__name}] {s}"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						def consume_data(self, data: dict):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert isinstance(data, dict), (
 | 
				
			||||||
 | 
								f"Data should be a dict (found: {type(data).__name__}) "
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__data = data
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert "config-file-name" in self.__data.keys(), (
 | 
				
			||||||
 | 
								f"Name of ssh config file must be present at config-file-name"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							config_file_name = self.__data["config-file-name"]
 | 
				
			||||||
 | 
							assert isinstance(config_file_name, str), (
 | 
				
			||||||
 | 
								f"config-file-name must be a string, but got: {type(config_file_name).__name__}"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							self.__ssh_config_file_name = config_file_name
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if "adapters" in self.__data.keys():
 | 
				
			||||||
 | 
								adapters = self.__data["adapters"]
 | 
				
			||||||
 | 
								if isinstance(adapters, list):
 | 
				
			||||||
 | 
									pass
 | 
				
			||||||
 | 
								elif isinstance(adapters, str):
 | 
				
			||||||
 | 
									adapters = [adapters]
 | 
				
			||||||
 | 
								else:
 | 
				
			||||||
 | 
									raise AssertionError(f"Unsupported adapters data type: {type(adapters).__name__}")
 | 
				
			||||||
 | 
								self.__adapters_names.extend(adapters)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if "adapter" in self.__data.keys():
 | 
				
			||||||
 | 
								adapters = self.__data["adapter"]
 | 
				
			||||||
 | 
								if isinstance(adapters, list):
 | 
				
			||||||
 | 
									pass
 | 
				
			||||||
 | 
								elif isinstance(adapters, str):
 | 
				
			||||||
 | 
									adapters = [adapters]
 | 
				
			||||||
 | 
								else:
 | 
				
			||||||
 | 
									raise AssertionError(f"Unsupported adapter data type: {type(adapters).__name__}")
 | 
				
			||||||
 | 
								self.__adapters_names.extend(adapters)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if "ssids" in self.__data.keys():
 | 
				
			||||||
 | 
								ssids = self.__data["ssids"]
 | 
				
			||||||
 | 
								if isinstance(ssids, list):
 | 
				
			||||||
 | 
									pass
 | 
				
			||||||
 | 
								elif isinstance(ssids, str):
 | 
				
			||||||
 | 
									ssids = [ssids]
 | 
				
			||||||
 | 
								else:
 | 
				
			||||||
 | 
									raise AssertionError(f"Unsupported ssids data type: {type(ssids).__name__}")
 | 
				
			||||||
 | 
								self.__ssids.extend(ssids)
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
							if "ssid" in self.__data.keys():
 | 
				
			||||||
 | 
								ssids = self.__data["ssid"]
 | 
				
			||||||
 | 
								if isinstance(ssids, list):
 | 
				
			||||||
 | 
									pass
 | 
				
			||||||
 | 
								elif isinstance(ssids, str):
 | 
				
			||||||
 | 
									ssids = [ssids]
 | 
				
			||||||
 | 
								else:
 | 
				
			||||||
 | 
									raise AssertionError(f"Unsupported ssid data type: {type(ssids).__name__}")
 | 
				
			||||||
 | 
								self.__ssids.extend(ssids)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert len(self.__adapters_names) > 0, (
 | 
				
			||||||
 | 
								f"At least one adapter must be configured at target-name::adapters"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def name(self) -> str:
 | 
				
			||||||
 | 
							return self.__name
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def ssh_config_file_name(self) -> str:
 | 
				
			||||||
 | 
							return self.__ssh_config_file_name
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def adapters(self) -> list[str]:
 | 
				
			||||||
 | 
							return self.__adapters_names
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def ssids(self) -> list[str]:
 | 
				
			||||||
 | 
							return self.__ssids
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Config:
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						__DEFAULT_NORMAL_SSH_CONFIG_FILE_NAME = "config"
 | 
				
			||||||
 | 
						__DEFAULT_SSH_DIRECTORY_NAME = ".ssh"
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def __init__(self, logger: Logger, file_path: str):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__logger = logger
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if isinstance(file_path, str):
 | 
				
			||||||
 | 
								file_path = Path(file_path)
 | 
				
			||||||
 | 
							elif isinstance(file_path, Path):
 | 
				
			||||||
 | 
								pass
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								raise AssertionError("File path should be a string or Path object")
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__file_path = file_path
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__data = {}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__dry_run = False
 | 
				
			||||||
 | 
							self.__ssh_dir = Path(os.path.expanduser("~")) / self.__DEFAULT_SSH_DIRECTORY_NAME
 | 
				
			||||||
 | 
							# noinspection PyTypeChecker
 | 
				
			||||||
 | 
							self.__default_target_name: str = None
 | 
				
			||||||
 | 
							self.__targets = {}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self._load_config()
 | 
				
			||||||
 | 
							self._consume_config()
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							print(self)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def __str__(self):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							s = ""
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							s += "*** Config ***"
 | 
				
			||||||
 | 
							s += "\n Dry run: " + "True" if self.__dry_run else "False"
 | 
				
			||||||
 | 
							for target in self.__targets.values():
 | 
				
			||||||
 | 
								s += "\n" + str(target)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return s
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def _load_config(self):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert self.__file_path.exists(), "Config file must exist"
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							with open(self.__file_path) as f:
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								self.__data = yaml.safe_load(f)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def _consume_config(self):
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert isinstance(self.__data, dict), (
 | 
				
			||||||
 | 
								f"Config data must be a dict"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert "options" in self.__data.keys(), (
 | 
				
			||||||
 | 
								f"Options key missing from config"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							options = self.__data["options"]
 | 
				
			||||||
 | 
							assert isinstance(options, dict), "Config options must be a dict!"
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if "dry-run" in options.keys():
 | 
				
			||||||
 | 
								d = options["dry-run"]
 | 
				
			||||||
 | 
								assert isinstance(d, bool), "options::dry-run must be a bool"
 | 
				
			||||||
 | 
								if d:
 | 
				
			||||||
 | 
									self.__logger.complain(f"Dry run enabled in config")
 | 
				
			||||||
 | 
								self.__dry_run = d
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if "ssh-dir" in options.keys():
 | 
				
			||||||
 | 
								ssh_dir = Path(options["ssh-dir"])
 | 
				
			||||||
 | 
								assert ssh_dir.exists(), f"options::ssh-dir must be a valid directory"
 | 
				
			||||||
 | 
								self.__ssh_dir = ssh_dir
 | 
				
			||||||
 | 
								self.__logger.log(f"Found ssh dir: {self.__ssh_dir}")
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								self.__logger.log(f"options::ssh-dir not found")
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert "default-target" in options.keys(), (
 | 
				
			||||||
 | 
								f"Must specify the name of the default target at options::default-target"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							default_target_name = options["default-target"]
 | 
				
			||||||
 | 
							assert isinstance(default_target_name, str), (
 | 
				
			||||||
 | 
								f"Default target name must be a string but got: {type(default_target_name).__name__}"
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							self.__default_target_name = default_target_name
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							self.__targets = {}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							assert "targets" in self.__data.keys(), "Config should specify targets"
 | 
				
			||||||
 | 
							targets = self.__data["targets"]
 | 
				
			||||||
 | 
							assert isinstance(targets, dict), "Targets should be a dict, where each key is one target"
 | 
				
			||||||
 | 
							for target_name in targets.keys():
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								self.__logger.log(f"Parsing target: {target_name}")
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								try:
 | 
				
			||||||
 | 
									t = Target(
 | 
				
			||||||
 | 
										logger=self.__logger,
 | 
				
			||||||
 | 
										name=target_name,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									t.consume_data(data=targets[target_name])
 | 
				
			||||||
 | 
								except AssertionError as e:
 | 
				
			||||||
 | 
									self.__logger.complain(
 | 
				
			||||||
 | 
										f"Failed to parse target \"{target_name}\""
 | 
				
			||||||
 | 
										f"\n{e}"
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									raise e
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								self.__targets[target_name] = t
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if self.__default_target_name not in self.__targets.keys():
 | 
				
			||||||
 | 
								raise AssertionError(
 | 
				
			||||||
 | 
									f"Default target specified as {self.__default_target_name} but was not found in dict of targets"
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def default_normal_ssh_config_file_name(self) -> str:
 | 
				
			||||||
 | 
							return self.__DEFAULT_NORMAL_SSH_CONFIG_FILE_NAME
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def file_path(self) -> Path:
 | 
				
			||||||
 | 
							return self.__file_path
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def dry_run(self) -> bool:
 | 
				
			||||||
 | 
							return self.__dry_run
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@dry_run.setter
 | 
				
			||||||
 | 
						def dry_run(self, b: bool):
 | 
				
			||||||
 | 
							self.__dry_run = b
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def default_target_name(self) -> str:
 | 
				
			||||||
 | 
							return self.__default_target_name
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def ssh_dir(self) -> Path | None:
 | 
				
			||||||
 | 
							return self.__ssh_dir
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						@property
 | 
				
			||||||
 | 
						def targets(self) -> [Target]:
 | 
				
			||||||
 | 
							return self.__targets
 | 
				
			||||||
							
								
								
									
										41
									
								
								init-environment
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										41
									
								
								init-environment
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log()
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						echo "[Connection Specific SSH Config - Init Env] $1"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					complain()
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						echo "[Connection Specific SSH Config - Init Env] $1" 1>&2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					die()
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						complain "Fatal: $1"
 | 
				
			||||||
 | 
						exit 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SCRIPT_PATH=$(readlink -f "$0")
 | 
				
			||||||
 | 
					SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
 | 
				
			||||||
 | 
					SCRIPT_NAME=$(basename "$SCRIPT_PATH")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log "Begin ${SCRIPT_NAME}"
 | 
				
			||||||
 | 
					log "Script path: ${SCRIPT_PATH}"
 | 
				
			||||||
 | 
					log "Script dir: ${SCRIPT_DIR}"
 | 
				
			||||||
 | 
					log "PATH: ${PATH}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log "PWD before switching: $(pwd)"
 | 
				
			||||||
 | 
					cd "${SCRIPT_DIR}" || die "Failed to switch to project directory: ${SCRIPT_DIR}"
 | 
				
			||||||
 | 
					log "PWD after switching: $(pwd)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log "Ensuring python installation with pyenv"
 | 
				
			||||||
 | 
					pyenv versions
 | 
				
			||||||
 | 
					pyenv install --skip-existing || die "Failed to ensure python installation with pyenv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log "Installing/upgrading pip and pipenv"
 | 
				
			||||||
 | 
					pip install --upgrade pip pipenv || die "Failed to install/upgrade pip and pipenv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					log "Syncing pip environment with pipenv"
 | 
				
			||||||
 | 
					pipenv --rm  # Don't die because this will return an error if the env didn't already exist
 | 
				
			||||||
 | 
					pipenv install || die "Failed to install pip environment with pipenv"
 | 
				
			||||||
 | 
					pipenv sync || die "Failed to sync pip environment with pipenv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										58
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from domain.SSHConfigChanger import SSHConfigChanger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						parser = argparse.ArgumentParser(
 | 
				
			||||||
 | 
							prog="Mike's SSH Config Changer"
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						parser.add_argument(
 | 
				
			||||||
 | 
							"--dry-run", "-d",
 | 
				
			||||||
 | 
							dest="dry_run",
 | 
				
			||||||
 | 
							default=False,
 | 
				
			||||||
 | 
							action="store_true",
 | 
				
			||||||
 | 
							help="Pass this flag to print what would be changed, without actually changing anything"
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						parser.add_argument(
 | 
				
			||||||
 | 
							"--config", "-c",
 | 
				
			||||||
 | 
							dest="config_file",
 | 
				
			||||||
 | 
							required=True,
 | 
				
			||||||
 | 
							help="Specify path to configuration file",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						parser.add_argument(
 | 
				
			||||||
 | 
							"--interface", "--action-interface",
 | 
				
			||||||
 | 
							dest="action_interface",
 | 
				
			||||||
 | 
							required=True,
 | 
				
			||||||
 | 
							help="Specify the interface specified by NetworkManager's action"
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						parser.add_argument(
 | 
				
			||||||
 | 
							"--command", "--action-command",
 | 
				
			||||||
 | 
							dest="action_command",
 | 
				
			||||||
 | 
							required=True,
 | 
				
			||||||
 | 
							help="Specify the command specified by NetworkManager's action"
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						args = parser.parse_args()
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						configger = SSHConfigChanger(
 | 
				
			||||||
 | 
							dry_run=args.dry_run,
 | 
				
			||||||
 | 
							action_interface=args.action_interface,
 | 
				
			||||||
 | 
							action_command=args.action_command,
 | 
				
			||||||
 | 
							config_file_path=args.config_file,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						configger.run()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#   Main Entry
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
						main()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Reference in New Issue
	
	Block a user